yiisoft/yii2 · error · yii\base\InvalidArgumentException

$mode must be an integer or null.

Error message

$mode must be an integer or null.

What it means

changeOwnership() passes $mode straight to chmod(), which requires a PHP integer; strings like '0644' — the shell habit — fail is_int() and throw InvalidArgumentException before any filesystem call. PHP never auto-converts octal strings, and the octal-looking text is a string, so the API rejects it up front.

Source

Thrown at framework/helpers/BaseFileHelper.php:1004

                $user = $ownership;
            } elseif (is_string($ownership)) {
                $ownerParts = explode(':', $ownership);
                $user = $ownerParts[0];
                if (count($ownerParts) > 1) {
                    $group = $ownerParts[1];
                }
            } elseif (is_array($ownership)) {
                $ownershipIsIndexed = ArrayHelper::isIndexed($ownership);
                $user = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 0 : 'user');
                $group = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 1 : 'group');
            } else {
                throw new InvalidArgumentException('$ownership must be an integer, string, array, or null.');
            }
        }

        if ($mode !== null) {
            if (!is_int($mode)) {
                throw new InvalidArgumentException('$mode must be an integer or null.');
            }
            if (!chmod($path, $mode)) {
                throw new Exception('Unable to change mode of "' . $path . '" to "0' . decoct($mode) . '".');
            }
        }
        if ($user !== null && $user !== '') {
            if (is_numeric($user)) {
                $user = (int) $user;
            } elseif (!is_string($user)) {
                throw new InvalidArgumentException('The user part of $ownership must be an integer, string, or null.');
            }
            if (!chown($path, $user)) {
                throw new Exception('Unable to change user ownership of "' . $path . '" to "' . $user . '".');
            }
        }
        if ($group !== null && $group !== '') {
            if (is_numeric($group)) {
                $group = (int) $group;

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Use an octal integer literal: 0644, 0755 — never the quoted string form.
  2. Cast config strings with base 8: $mode = intval($config['mode'], 8);
  3. Validate before calling: is_int($mode), or is_string($mode) && ctype_digit($mode) followed by an intval(..., 8) cast.

Example fix

// before
\yii\helpers\FileHelper::changeOwnership($file, 'www-data', '0755');

// after
$mode = intval('0755', 8); // int 493
\yii\helpers\FileHelper::changeOwnership($file, 'www-data', $mode);
Defensive patterns

Strategy: type-guard

Validate before calling

if (is_string($mode)) {
    $mode = intval($mode, 8); // '0644' → 420
}
if (!is_int($mode)) {
    throw new \InvalidArgumentException('Mode must be an octal int, e.g. 0644');
}
\yii\helpers\FileHelper::changeOwnership($path, 'www-data', $mode);

Type guard

/** @param mixed $mode */
function isModeValue($mode): bool
{
    return $mode === null || is_int($mode) || (is_string($mode) && ctype_digit($mode));
}

Prevention

When it happens

Trigger: FileHelper::changeOwnership($path, 'www-data', '0644'); a mode loaded from JSON/DB config, where values always arrive as strings; a mode computed as '0' . decoct($m) and not cast back; passing mode from a framework-agnostic config array shared with shell scripts.

Common situations: Config-driven permission settings where YAML parses 0644 as int but JSON keeps '0644' a string; porting shell snippets (chmod 0644 ...) to PHP; modes stored in environment variables.

Related errors


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/455cf98b5ea0687b. Report an issue: GitHub.