> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Extensions

> See what PHP extensions are available with each PHP version on Upsun.

export const MetaExtensionPhpGrid = () => {
  const [extensions, setExtensions] = useState({});
  const [versions, setVersions] = useState([]);
  const [versionStatuses, setVersionStatuses] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const STORAGE_KEY = 'upsun_php_extensions_cache';
  const IMAGES_STORAGE_KEY = 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/extensions/php/cloud';
  const IMAGES_API_URL = 'https://meta.upsun.com/images';
  useEffect(() => {
    setLoading(true);
    setError(null);
    const fetchCached = async (key, url) => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(key);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) return cachedData;
          }
        } catch (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(url, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(key, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(key, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    Promise.all([fetchCached(STORAGE_KEY, API_URL), fetchCached(IMAGES_STORAGE_KEY, IMAGES_API_URL)]).then(([extData, imagesData]) => {
      if (!extData) {
        setExtensions({});
        setVersions([]);
        setLoading(false);
        return;
      }
      const statuses = {};
      if (imagesData && imagesData.php && imagesData.php.versions) {
        for (const [ver, verData] of Object.entries(imagesData.php.versions)) {
          statuses[ver] = verData.upsun?.status || verData.status || '';
        }
      }
      setVersionStatuses(statuses);
      const allVersions = new Set();
      const transformedExtensions = {};
      for (const extName of Object.keys(extData)) {
        const ext = extData[extName];
        transformedExtensions[extName] = {};
        if (ext.versions && typeof ext.versions === 'object') {
          for (const [versionKey, versionData] of Object.entries(ext.versions)) {
            allVersions.add(versionKey);
            const options = Array.isArray(versionData.options) ? versionData.options : [];
            switch (versionData.status) {
              case 'available':
                {
                  transformedExtensions[extName][versionKey] = {
                    status: 'available',
                    options
                  };
                  break;
                }
              case 'built-in':
                {
                  transformedExtensions[extName][versionKey] = {
                    status: 'built-in',
                    options
                  };
                  break;
                }
              case 'default':
                {
                  transformedExtensions[extName][versionKey] = {
                    status: 'default',
                    options
                  };
                  break;
                }
              default:
                {
                  transformedExtensions[extName][versionKey] = {
                    status: '',
                    options
                  };
                }
            }
          }
        }
      }
      const sortedVersions = [...allVersions].sort((a, b) => {
        const aNum = Number.parseFloat(a.replace('x', '9'));
        const bNum = Number.parseFloat(b.replace('x', '9'));
        return bNum - aNum;
      });
      setVersions(sortedVersions);
      setExtensions(transformedExtensions);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaExtensionPhpGrid error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, []);
  const scrollRef = useRef(null);
  const leftColRef = useRef(null);
  const headerScrollRef = useRef(null);
  const extensionNames = Object.keys(extensions).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  const extHasOptions = extName => {
    for (const v of versions) {
      const cell = extensions[extName]?.[v];
      if (cell && cell.options && cell.options.length > 0) return true;
    }
    return false;
  };
  if (loading) return <p>Loading PHP extensions...</p>;
  if (error) return <p>Error: {error}</p>;
  if (extensionNames.length === 0) return <p>No PHP extensions found.</p>;
  const rowHeight = '36px';
  const rowHeightWithOptions = '58px';
  const headerHeight = '40px';
  const borderColor = 'var(--color-bg-alt, #e5e7eb)';
  const totalVersionsWidth = versions.length * 70;
  return <div>
      <style>{`
        .php-ext-left-col::-webkit-scrollbar { display: none; }
        .php-ext-left-col { scrollbar-width: none; }
      `}</style>
      {}
      <div style={{
    display: 'flex',
    justifyContent: 'flex-end',
    marginBottom: '8px'
  }}>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px',
    backgroundColor: 'var(--color-bg-alt, #f0ecff)',
    borderRadius: '6px',
    padding: '5px 10px'
  }}>
          <span style={{
    fontSize: '0.75rem',
    fontWeight: 500,
    color: 'var(--color-accent-light, #6046ff)'
  }}>Scroll for older versions →</span>
        </div>
      </div>
      <div style={{
    borderRadius: '8px',
    border: `1px solid ${borderColor}`,
    overflow: 'hidden'
  }}>
      {}
      <div style={{
    display: 'flex',
    borderBottom: `2px solid ${borderColor}`,
    backgroundColor: 'var(--color-bg-card)'
  }}>
        <div style={{
    flexShrink: 0,
    width: '190px',
    height: headerHeight,
    display: 'flex',
    alignItems: 'center',
    padding: '0 1.5rem 0 1rem',
    fontWeight: 600,
    fontSize: '0.8rem',
    color: 'var(--color-text-heading)',
    borderRight: `2px solid ${borderColor}`
  }}>Extension</div>
        <div ref={headerScrollRef} style={{
    flex: 1,
    overflow: 'hidden'
  }}>
          <div style={{
    display: 'flex',
    minWidth: `${totalVersionsWidth}px`
  }}>
            {versions.map(v => {
    const vStatus = versionStatuses[v] || '';
    let headerBg = 'transparent';
    let headerColor = 'var(--color-text-heading, #374151)';
    let statusLabel = '';
    if (vStatus === 'supported') {
      headerBg = 'rgba(16, 185, 129, 0.12)';
      headerColor = '#10b981';
      statusLabel = 'supported';
    } else if (vStatus === 'deprecated') {
      headerBg = 'rgba(245, 158, 11, 0.12)';
      headerColor = '#f59e0b';
      statusLabel = 'deprecated';
    } else if (vStatus === 'retired' || vStatus === 'eol') {
      headerBg = 'rgba(239, 68, 68, 0.12)';
      headerColor = '#ef4444';
      statusLabel = 'retired';
    }
    return <div key={v} style={{
      minWidth: '70px',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      padding: '0 0.5rem',
      whiteSpace: 'nowrap',
      height: headerHeight
    }}>
                  <span style={{
      fontWeight: 600,
      fontSize: '0.8rem',
      color: 'var(--color-text-heading)'
    }}>{v}</span>
                  {statusLabel && <span style={{
      fontSize: '0.55rem',
      fontWeight: 500,
      color: headerColor,
      backgroundColor: headerBg,
      borderRadius: '3px',
      padding: '1px 4px',
      textTransform: 'uppercase',
      letterSpacing: '0.02em'
    }}>{statusLabel}</span>}
                </div>;
  })}
          </div>
        </div>
      </div>
      {}
      <div style={{
    display: 'flex',
    maxHeight: '60vh'
  }}>
        <div ref={leftColRef} className="php-ext-left-col" style={{
    flexShrink: 0,
    width: '190px',
    borderRight: `2px solid ${borderColor}`,
    overflowY: 'auto',
    overflowX: 'hidden'
  }}>
          {extensionNames.map((ext, i) => <div key={ext} style={{
    height: extHasOptions(ext) ? rowHeightWithOptions : rowHeight,
    display: 'flex',
    alignItems: 'center',
    padding: '0 1.5rem 0 1rem',
    fontWeight: 500,
    borderBottom: `1px solid ${borderColor}`,
    backgroundColor: i % 2 === 0 ? 'var(--color-bg-card)' : 'var(--color-bg-alt)'
  }}><code style={{
    fontSize: '0.85rem'
  }}>{ext}</code></div>)}
          {}
          <div style={{
    minHeight: '16px',
    flexShrink: 0
  }} />
        </div>
        <div ref={scrollRef} style={{
    flex: 1,
    overflowX: 'auto',
    overflowY: 'auto'
  }} onScroll={e => {
    if (leftColRef.current) {
      leftColRef.current.scrollTop = e.target.scrollTop;
    }
    if (headerScrollRef.current) {
      headerScrollRef.current.scrollLeft = e.target.scrollLeft;
    }
  }}>
          {extensionNames.map((ext, i) => {
    const hasOptions = extHasOptions(ext);
    return <div key={ext} style={{
      display: 'flex',
      height: hasOptions ? rowHeightWithOptions : rowHeight,
      minWidth: `${totalVersionsWidth}px`,
      borderBottom: `1px solid ${borderColor}`,
      backgroundColor: i % 2 === 0 ? 'var(--color-bg-card)' : 'var(--color-bg-alt)'
    }}>
              {versions.map(v => {
      const cell = extensions[ext][v];
      const status = cell?.status || '';
      const options = cell?.options || [];
      let display = '';
      let color = 'inherit';
      if (status === 'available') {
        display = '●';
        color = 'var(--color-accent-light)';
      } else if (status === 'built-in') {
        display = '✔*';
        color = '#10b981';
      } else if (status === 'default') {
        display = '✔';
        color = '#10b981';
      }
      return <div key={v} style={{
        minWidth: '70px',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '0.95rem',
        color,
        padding: '0 0.5rem',
        gap: '2px'
      }}>
                    <span>{display}</span>
                    {options.length > 0 && <div style={{
        display: 'flex',
        flexWrap: 'wrap',
        gap: '2px',
        justifyContent: 'center'
      }}>
                        {options.map(opt => <span key={opt} style={{
        fontSize: '0.55rem',
        fontWeight: 500,
        color: '#4b5563',
        backgroundColor: '#f3f4f6',
        borderRadius: '3px',
        padding: '1px 4px'
      }}>{opt}</span>)}
                      </div>}
                  </div>;
    })}
            </div>;
  })}
        </div>
      </div>
      </div>
    </div>;
};

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

<Info>
  You can use the [Upsun composable image](/docs/configure-apps/app-reference/composable-image) to install multiple runtimes and tools in your application container.
  When using the composable image, see how you can:

  * [Manage PHP extensions](/docs/configure-apps/app-reference/composable-image#php-extensions-and-python-packages)
  * [Modify your PHP runtime](/docs/languages/php#modify-your-php-runtime-when-using-the-composable-image)
</Info>

PHP has a number of [extensions](https://pecl.php.net/) developed by members of the community.
Some of them are available for Upsun containers.

You can define the PHP extensions you want to enable or disable:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          runtime:
            extensions:
              - raphf
              - http
              - igbinary
              - redis
            disabled_extensions:
              - sqlite3`
  }
</DynamicCodeBlock>

You can also [include configuration options](/docs/configure-apps/app-reference/single-runtime-image#extensions) for specific extensions.

The following table shows all extensions that are available (●) and on by default (✔).
You can turn on the available ones with the `extensions` key
and turn off those on by default with the `disabled_extensions` key.

<MetaExtensionPhpGrid />

Some built-in modules are always on:

* `date`
* `filter`
* `hash`
* `json` (from 8.0)
* `libxml`
* `openssl`
* `pcntl`
* `pcre`
* `Reflection`
* `session`
* `SPL`
* `standard`
* `Zend OPcache` (from 5.5)
* `zlib`

To see a complete list of the compiled PHP extensions, run the following [CLI command](/cli):

```bash theme={null}
upsun ssh "php -m"
```

## Custom PHP extensions

It's possible to use an extension not listed here,
but it takes slightly more work:

1. Download the `.so` file for the extension as part of your build hook using `curl` or similar.
   It can also be added to your Git repository if the file isn't publicly downloadable,
   but committing large binary blobs to Git is generally not recommended.

2. Load the extension using an absolute path by [customizing the PHP settings](/docs/languages/php#customize-php-settings)
   For example, if the extension is named `spiffy.so` and is in your [app root](/docs/configure-apps/app-reference/single-runtime-image#root-directory),
   your configuration looks like the following:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          variables:
            php:
              extension: /app/spiffy.so`
  }
</DynamicCodeBlock>
