skip to content

In PHP, how are extra .ini files in a conf.d scan directory loaded, and when do you use zend_extension= instead of extension=?

level: middleimportance: should knowfreq 32%

answer

  1. parsed after php.ini, alphabetical order
  2. PHP_INI_SCAN_DIR overrides the compiled path
  3. empty entry means the compiled default
  4. zend_extension for engine hooks, like Xdebug
  5. php -m lists [Zend Modules]

basics

~20 s

After php.ini, PHP parses every .ini file in the scan directory in alphabetical order; PHP_INI_SCAN_DIR overrides that directory. extension= loads a regular extension, while zend_extension= loads one that hooks the Zend engine itself, such as Xdebug.

solid answer

~40 s

The **scan directory**, often called `conf.d`, is set at build time with `--with-config-file-scan-dir`. After php.ini, PHP parses every file ending in `.ini` there **in alphabetical order**, which is why packages name files like `20-intl.ini`; a later file wins when two set the same directive. The **`PHP_INI_SCAN_DIR`** environment variable overrides the directory and can list several, separated by `:` on Unix; an empty entry stands for the compiled-in default, so `:/extra` keeps it and adds another. `extension=intl` loads an ordinary extension that adds functions and classes. `zend_extension=xdebug` loads a **Zend extension**, which hooks the engine's compilation or execution. `php -m` lists them under `[PHP Modules]` and `[Zend Modules]`. In PHP 8.5 OPcache is built in, and `zend_extension=opcache.so` now only emits a warning.

code

bash · 10 lines
bash
PHP_INI_SCAN_DIR=:/app/php.d php --ini
# Scan for additional .ini files in: ":/app/php.d"
# Additional .ini files parsed:      /usr/local/etc/php/conf.d/20-intl.ini,
#   /app/php.d/99-app.ini

php -m
# [PHP Modules]
# ...
# [Zend Modules]
# Zend OPcache

go deeper

for a junior

Know that extra .ini files in a conf.d directory are loaded after php.ini and that extensions are enabled with extension= lines.

for a middle

Explain alphabetical loading and last-wins, PHP_INI_SCAN_DIR with the empty-entry trick, and why debuggers need zend_extension=.

for a senior

Structure container and server configuration as ordered scan files, verify with php --ini and php -m, and clean up stale lines such as opcache.so on 8.5.

for a principal

Define how configuration is layered across images and environments so overrides are explicit, ordered and reviewable instead of edited in place.

## Why a scan directory exists One large php.ini is hard to manage when packages, containers and configuration tools each want to add settings. PHP therefore supports a **scan directory**: after reading php.ini, it parses every `.ini` file in that directory. Linux packages and container images use it to drop in one file per extension or concern. ## How files are loaded 1. PHP reads the main php.ini. 2. It reads the scan directory set at build time with `--with-config-file-scan-dir`, or the one given by `PHP_INI_SCAN_DIR`. 3. Within each directory it parses every file ending in **`.ini`**, in **alphabetical order**. 4. When two files set the same directive, the one parsed **later** wins. The alphabetical rule is why files carry numeric prefixes: `10-opcache-tuning.ini`, `20-intl.ini`, `99-overrides.ini`. A local override file named to sort last reliably beats the package defaults. ## PHP_INI_SCAN_DIR The environment variable overrides the compiled-in scan directory: | Value | Directories scanned | |---|---| | unset | the compiled-in directory | | `/app/php.d` | only `/app/php.d` | | `:/app/php.d` | the compiled-in directory, then `/app/php.d` | | `/app/php.d:` | `/app/php.d`, then the compiled-in directory | | empty string | none | The separator is the platform's path separator, `:` on Unix and `;` on Windows. An **empty entry** in the list means the compiled-in default, which is how the leading or trailing colon forms work. This is handy in containers and CI, where an app ships its own `php.d` without replacing the image's defaults. `php --ini` and `php_ini_scanned_files()` show exactly which files were parsed and in what order. ## extension= versus zend_extension= Both directives load a shared library at process startup, so they belong in configuration read at startup (php.ini, a scan-directory file, or `-d` on the command line), never in `ini_set()`, `.user.ini` or `.htaccess`. The difference is what the library plugs into: | Directive | Loads | Typical examples | |---|---|---| | `extension=` | a regular PHP extension that registers functions, classes, constants and ini settings | `intl`, `pdo_pgsql`, `gd`, `sodium` | | `zend_extension=` | a **Zend extension** that hooks the engine itself, for example compilation, execution or the debugger interface | Xdebug, which steps through and profiles code | For `extension=`, the shipped php.ini recommends the bare name (`extension=mysqli`) and resolves it in `extension_dir`; a full path also works. `php -m` prints the loaded modules in two sections, `[PHP Modules]` and `[Zend Modules]`, and `get_loaded_extensions(true)` returns the Zend ones from code. ## OPcache in PHP 8.5 OPcache used to be the best-known `zend_extension`. Since PHP 8.5 it is **always built into the binary and always loaded**; the build no longer produces `opcache.so`, and a leftover `zend_extension=opcache.so` line emits a warning. Its own settings are a separate topic. ## Environment values inside ini files Any ini file can read environment variables with `${NAME}`, and since PHP 8.3 a fallback can be given with `${NAME:-default}`, as in `memory_limit = ${PHP_MEMORY_LIMIT:-256M}`. Combined with a scan directory this lets one image serve several environments. ## Checking what loaded - `php --ini` lists the scan directory and every file parsed, in order. - `php -m` lists loaded modules, with Zend extensions under their own heading. - `extension_loaded('intl')` returns `true` or `false` from code, which suits a startup self-check. - Remember that each SAPI loads its own configuration, so run these checks under the SAPI you care about. ## Common mistakes - **Loading an extension twice**, once from php.ini and once from a scan file, which produces a startup warning that the module is already loaded. - **Loading Xdebug with `extension=`**: it must be loaded as a Zend extension. - **A file without the `.ini` suffix**, such as `xdebug.conf`, which the scan silently skips.

  • Two scan-directory files set memory_limit differently. Which value wins?
    The one in the file parsed later. PHP parses scan-directory files in alphabetical order after php.ini, so the name that sorts last wins, which is why override files use high numeric prefixes such as 99-.
  • How do you add an app's ini directory without losing the image's defaults?
    Set PHP_INI_SCAN_DIR with an empty entry for the compiled-in directory, for example :/app/php.d on Unix. PHP scans the default directory first and then /app/php.d.
  • What happens with zend_extension=opcache.so on PHP 8.5?
    It emits a warning. OPcache is compiled into the PHP 8.5 binary and always loaded, and the build no longer produces opcache.so, so the line should be removed; opcache.* settings still apply.

saying these in an interview costs you the question

  • Scan-directory files are loaded in the order they were created.
  • PHP_INI_SCAN_DIR=/app/php.d adds a directory to the default one.
  • Xdebug can be loaded with a plain extension= line.
  • Files in conf.d are read before php.ini, so php.ini always wins.
  • In PHP 8.5, OPcache still needs zend_extension=opcache.so.