yiisoft/yii2 · error · yii\base\InvalidArgumentException

Exclude/include pattern must be a string.

Error message

Exclude/include pattern must be a string.

What it means

parseExcludePattern() is the single funnel that turns a glob string into the internal ['pattern', 'flags', 'firstWildcard'] structure, and it starts with a defensive type assertion: anything that is not a PHP string throws InvalidArgumentException. Note that both public call paths (normalizeOptions() and lastExcludeMatchingFromList()) guard the call with is_string(), so through the normal API a non-string pattern usually surfaces as the sibling error 262 instead; hitting 263 means a custom subclass or patched helper invoked the parser directly with a scalar/object.

Source

Thrown at framework/helpers/BaseFileHelper.php:871

            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
                return $exclude;
            }
        }

        return null;
    }

    /**
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
     * @param string $pattern
     * @param bool $caseSensitive
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
     * @throws InvalidArgumentException
     */
    private static function parseExcludePattern($pattern, $caseSensitive)
    {
        if (!is_string($pattern)) {
            throw new InvalidArgumentException('Exclude/include pattern must be a string.');
        }

        $result = [
            'pattern' => $pattern,
            'flags' => 0,
            'firstWildcard' => false,
        ];

        if (!$caseSensitive) {
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
        }

        if (empty($pattern)) {
            return $result;
        }

        if (strncmp($pattern, '!', 1) === 0) {
            $result['flags'] |= self::PATTERN_NEGATIVE;

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Make every entry of 'only'/'except' a plain string glob in config and call sites.
  2. If subclassing, keep the is_string() guard before any pattern parsing.
  3. Sanitize pattern arrays before passing them: array_filter($patterns, 'is_string') or map non-strings to their string form.
  4. Add a unit assertion over config-driven pattern lists so type drift is caught in CI.

Example fix

// before
$options = ['except' => [0, '*.log', new \stdClass()]];
$files = \yii\helpers\FileHelper::findFiles($dir, $options);

// after
$options = ['except' => ['*.log']];
$files = \yii\helpers\FileHelper::findFiles($dir, $options);
Defensive patterns

Strategy: type-guard

Validate before calling

// Keep pattern lists string-only before any FileHelper call
$options['except'] = array_values(array_filter($options['except'] ?? [], 'is_string'));
$options['only'] = array_values(array_filter($options['only'] ?? [], 'is_string'));

Type guard

/** @param array<int|string, mixed> $patterns @return list<string> */
function stringPatterns(array $patterns): array
{
    $out = [];
    foreach ($patterns as $p) {
        if (is_string($p)) { $out[] = $p; }
        elseif (is_scalar($p)) { $out[] = (string) $p; }
        // arrays/objects are dropped — they are not valid patterns
    }
    return $out;
}

Prevention

When it happens

Trigger: A non-string element (int, float, bool, object) in options['except']/options['only'] reaching the parser — typically via a subclass overriding normalizeOptions()/filterPath() without the is_string() guard; test code calling the private method via reflection; code that injects pre-parsed values into the pattern list mixed with raw non-string scalars.

Common situations: Custom FileHelper subclasses that pre-process pattern lists; config-driven pattern lists where a stray integer or null slips in; refactors that bypass the built-in string checks.

Related errors


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