Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
76cc276
fix: remove xdebug2 defaults from php.ini
AaronFeledy Mar 17, 2026
87c2d32
fix: use host.lando.internal for xdebug
AaronFeledy Mar 17, 2026
d91c37e
Add xdebug toggle helper script
AaronFeledy Mar 17, 2026
bf5f9c9
Add xdebug status output
AaronFeledy Mar 17, 2026
37e5a8e
Register default xdebug tooling
AaronFeledy Mar 17, 2026
d4b80b8
feat: normalize xdebug config
AaronFeledy Mar 17, 2026
c23e947
feat: generate dedicated xdebug ini
AaronFeledy Mar 17, 2026
6e348d0
fix: restore max_nesting_level default in generated xdebug ini
AaronFeledy Mar 17, 2026
e4c75ff
Add xdebug config to service info
AaronFeledy Mar 17, 2026
439c5e6
Expand xdebug leia coverage
AaronFeledy Mar 17, 2026
7d6654e
test: add unit tests for xdebug config normalization and fix leia dep…
AaronFeledy Mar 17, 2026
afd05fd
docs: rewrite xdebug documentation for new features
AaronFeledy Mar 17, 2026
4af9cdf
fix: update Chrome Web Store URL to new domain
AaronFeledy Mar 17, 2026
18cef14
Fix xdebug env var precedence issues
cursoragent Mar 17, 2026
86b5496
fix: CI test failures — xdebug.sh reload logic and leia test assertions
AaronFeledy Mar 17, 2026
e824d06
Fix test to check xdebug mode via php -i instead of XDEBUG_MODE env var
cursoragent Mar 17, 2026
7d543ee
fix: pin swoole version in php-extensions test (6.2.0 compile failure)
AaronFeledy Mar 17, 2026
a650420
Fix flaky xdebug leia assertions
AaronFeledy Mar 17, 2026
aa08622
Stabilize xdebug config leia checks
AaronFeledy Mar 17, 2026
f2bb200
Fix: Always generate xdebug ini file to persist custom settings
cursoragent Mar 18, 2026
89a5d67
Fix xdebug ini generation and add clarifying comments
AaronFeledy Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ config.*.timestamp-*-*.*

# YARN
yarn.lock
.agents/ralph
129 changes: 115 additions & 14 deletions builders/php.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

// Modules
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const semver = require('semver');
Expand Down Expand Up @@ -50,13 +51,75 @@ const nginxConfig = options => ({
version: options.via.split(':')[1],
});

const xdebugConfig = host => ([
`client_host=${host}`,
'discover_client_host=1',
'log=/tmp/xdebug.log',
'remote_enable=true',
`remote_host=${host}`,
].join(' '));
const xdebugConfig = (phpSemver, normalizedConfig) => {
const config = [
`client_host=${normalizedConfig.client_host}`,
'discover_client_host=1',
`log=${normalizedConfig.log === false ? '' : normalizedConfig.log}`,
];

if (phpSemver && semver.lt(phpSemver, '7.2.0')) {
config.push('remote_enable=true', `remote_host=${normalizedConfig.client_host}`);
}

return config.join(' ');
};

/**
* Normalizes xdebug configuration into a consistent object shape.
* @param {boolean|string|Object} xdebug - The user-provided xdebug configuration.
* @return {Object} The normalized xdebug configuration.
*/
const normalizeXdebugConfig = xdebug => {
const defaults = {
mode: 'off',
start_with_request: 'trigger',
client_host: 'host.lando.internal',
client_port: 9003,
log: '/tmp/xdebug.log',
idekey: '',
config: {},
};

if (xdebug === true) return _.merge({}, defaults, {mode: 'debug'});
if (xdebug === false || _.isNil(xdebug)) return _.merge({}, defaults, {mode: 'off'});
if (_.isString(xdebug)) return _.merge({}, defaults, {mode: xdebug});

if (_.isPlainObject(xdebug)) {
const normalized = _.merge({}, defaults, xdebug);
if (normalized.client_host === 'auto') normalized.client_host = 'host.lando.internal';
normalized.config = _.isPlainObject(normalized.config) ? normalized.config : {};
return normalized;
}

return _.merge({}, defaults, {mode: xdebug});
};

/**
* Generates xdebug ini contents from normalized xdebug configuration.
* @param {Object} config - The normalized xdebug configuration.
* @return {string} The generated xdebug ini contents.
*/
const generateXdebugIni = config => {
// Build as ordered map so config pass-through can override defaults without duplicates
const settings = new Map([
['mode', config.mode],
['max_nesting_level', 512],
['start_with_request', config.start_with_request],
['client_host', config.client_host],
['client_port', config.client_port],
]);

if (config.log !== false) settings.set('log', config.log);
if (!_.isEmpty(config.idekey)) settings.set('idekey', config.idekey);

// config pass-through: overrides defaults if keys overlap, appends otherwise
_.forEach(config.config, (value, key) => settings.set(key, value));

const lines = ['; Generated by Lando PHP plugin'];
settings.forEach((value, key) => lines.push(`xdebug.${key} = ${value}`));
return `${lines.join('\n')}\n`;
};

const detectDatabaseClient = (options, debug = () => {}) => {
if (options.db_client === false) return null;
Expand Down Expand Up @@ -150,7 +213,7 @@ const parseConfig = options => {
};

// Builder
module.exports = {
const phpBuilder = {
name: 'php',
config: {
version: '7.4',
Expand All @@ -173,8 +236,16 @@ module.exports = {
command: ['sh -c \'a2enmod rewrite headers expires && apache2-foreground\''],
composer_version: true,
phpServer: 'apache',
// PHP loads conf.d ini files alphabetically. We use filename prefixes to control load order:
// xxx-lando-default.ini — base PHP settings
// yyy-lando-xdebug.ini — generated xdebug config from .lando.yml (host-mounted)
// zzz-lando-xdebug.ini — runtime mode override written by `lando xdebug` toggle (container-only)
// zzz-lando-my-custom.ini — user custom php.ini overrides
// The zzz toggle file intentionally only contains xdebug.mode so it can override the yyy config
// at runtime without clobbering other xdebug settings (client_port, idekey, etc).
defaultFiles: {
_php: 'php.ini',
xdebug: 'yyy-lando-xdebug.ini',
vhosts: 'default.conf',
// server: @TODO? DO THE PEOPLE DEMAND IT?
},
Expand All @@ -185,6 +256,7 @@ module.exports = {
},
remoteFiles: {
_php: '/usr/local/etc/php/conf.d/xxx-lando-default.ini',
xdebug: '/usr/local/etc/php/conf.d/yyy-lando-xdebug.ini',
vhosts: '/etc/apache2/sites-enabled/000-default.conf',
php: '/usr/local/etc/php/conf.d/zzz-lando-my-custom.ini',
pool: '/usr/local/etc/php-fpm.d/zz-lando.conf',
Expand Down Expand Up @@ -219,8 +291,26 @@ module.exports = {
options.command.unshift('docker-php-entrypoint');
}

// If xdebug is set to "true" then map it to "debug"
if (options.xdebug === true) options.xdebug = 'debug';
options._xdebugConfig = normalizeXdebugConfig(options.xdebug);
options.xdebug = options._xdebugConfig.mode;
const xdebugFile = path.join(options.confDest, options.defaultFiles.xdebug);
try {
fs.mkdirSync(options.confDest, {recursive: true});
fs.writeFileSync(xdebugFile, generateXdebugIni(options._xdebugConfig));
} catch (err) {
throw new Error(`Failed to write xdebug config to ${xdebugFile}: ${err.message}`);
}
options.volumes.push(`${xdebugFile}:${options.remoteFiles.xdebug}`);

options._app.config.tooling = options._app.config.tooling || {};
if (_.get(options, '_app.config.tooling.xdebug') === undefined) {
options._app.config.tooling.xdebug = {
service: options.name,
description: 'Toggle Xdebug mode',
cmd: '/etc/lando/service/helpers/xdebug.sh',
user: 'root',
};
}

// for older generation models
if (_.includes(options.gen2, options.version)) options.suffix = '2';
Expand All @@ -231,15 +321,22 @@ module.exports = {
environment: _.merge({}, options.environment, {
PATH: options.path.join(':'),
LANDO_WEBROOT: `/app/${options.webroot}`,
XDEBUG_CONFIG: xdebugConfig(options._app.env.LANDO_HOST_IP),
XDEBUG_MODE: (options.xdebug === false) ? 'off' : options.xdebug,
XDEBUG_CONFIG: xdebugConfig(phpSemver, options._xdebugConfig),
}),
networks: (_.startsWith(options.via, 'nginx')) ? {default: {aliases: ['fpm']}} : {default: {}},
ports: (_.startsWith(options.via, 'apache') && options.version !== 'custom') ? ['80'] : [],
volumes: options.volumes,
command: options.command.join(' '),
};
options.info = {via: options.via};
options.info = {
via: options.via,
xdebug: {
mode: options._xdebugConfig.mode,
client_host: options._xdebugConfig.client_host,
client_port: options._xdebugConfig.client_port,
start_with_request: options._xdebugConfig.start_with_request,
},
};

// Determine the appropriate composer version to install if not specified
if (options.composer_version === true || options.composer_version === '') {
Expand All @@ -253,7 +350,7 @@ module.exports = {
addBuildStep(['touch /tmp/xdebug.log && chmod 666 /tmp/xdebug.log'], options._app, options.name, 'build_as_root_internal');

// Add build step to enable xdebug
if (options.xdebug) {
if (options._xdebugConfig.mode !== 'off') {
addBuildStep(['docker-php-ext-enable xdebug'], options._app, options.name, 'build_as_root_internal');
}

Expand Down Expand Up @@ -309,3 +406,7 @@ module.exports = {
}
},
};

module.exports = phpBuilder;
module.exports.normalizeXdebugConfig = normalizeXdebugConfig;
module.exports.generateXdebugIni = generateXdebugIni;
6 changes: 0 additions & 6 deletions config/php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ date.timezone = "UTC"
;; PACKAGE SETTINGS ;;
;;;;;;;;;;;;;;;;;;;;;;

; Xdebug
xdebug.max_nesting_level = 512
xdebug.remote_autostart = 1
xdebug.start_with_request = trigger
xdebug.mode = ${XDEBUG_MODE}

; Globals
expose_php = on
max_execution_time = 90
Expand Down
Loading
Loading