In PHP, how are extra .ini files in a conf.d scan directory loaded, and when do you use zend_extension= instead of extension=?
answer
- parsed after php.ini, alphabetical order
- PHP_INI_SCAN_DIR overrides the compiled path
- empty entry means the compiled default
- zend_extension for engine hooks, like Xdebug
- php -m lists [Zend Modules]
basics
~20 sAfter 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 sThe **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 linesPHP_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 OPcachego deeper
Know that extra .ini files in a conf.d directory are loaded after php.ini and that extensions are enabled with extension= lines.
Explain alphabetical loading and last-wins, PHP_INI_SCAN_DIR with the empty-entry trick, and why debuggers need zend_extension=.
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.
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.