{"record":{"id":"9efae28b12abb415","repo":"yiisoft/yii2","slug":"failed-to-create-directory-path-message","errorCode":null,"errorMessage":"Failed to create directory \"{$path}\": {message}","messagePattern":"Failed to create directory \"(.+?)\": (.+?)","errorType":"exception","errorClass":"yii\\base\\Exception","httpStatus":null,"severity":"error","filePath":"framework/helpers/BaseFileHelper.php","lineNumber":720,"sourceCode":"     * @throws \\yii\\base\\Exception if the directory could not be created (i.e. php error due to parallel changes)\n     */\n    public static function createDirectory($path, $mode = 0775, $recursive = true)\n    {\n        if (is_dir($path)) {\n            return true;\n        }\n        $parentDir = dirname($path);\n        // recurse if parent dir does not exist and we are not at the root of the file system.\n        if ($recursive && !is_dir($parentDir) && $parentDir !== $path) {\n            static::createDirectory($parentDir, $mode, true);\n        }\n        try {\n            if (!mkdir($path, $mode)) {\n                return false;\n            }\n        } catch (\\Exception $e) {\n            if (!is_dir($path)) {// https://github.com/yiisoft/yii2/issues/9288\n                throw new \\yii\\base\\Exception(\"Failed to create directory \\\"$path\\\": \" . $e->getMessage(), $e->getCode(), $e);\n            }\n        }\n        try {\n            return chmod($path, $mode);\n        } catch (\\Exception $e) {\n            throw new \\yii\\base\\Exception(\"Failed to change permissions for directory \\\"$path\\\": \" . $e->getMessage(), $e->getCode(), $e);\n        }\n    }\n\n    /**\n     * Performs a simple comparison of file or directory names.\n     *\n     * Based on match_basename() from dir.c of git 1.8.5.3 sources.\n     *\n     * @param string $baseName file or directory name to compare with the pattern\n     * @param string $pattern the pattern that $baseName will be compared against\n     * @param int|bool $firstWildcard location of first wildcard character in the $pattern\n     * @param int $flags pattern flags","sourceCodeStart":702,"sourceCodeEnd":738,"githubUrl":"https://github.com/yiisoft/yii2/blob/66f00d18a29b520f85e8e8f1e32d1e7e7b556cac/framework/helpers/BaseFileHelper.php#L702-L738","documentation":"Thrown by yii\\helpers\\BaseFileHelper::createDirectory() when the native PHP mkdir() call raised an exception and a follow-up is_dir($path) check confirms the directory still does not exist. Yii wraps the low-level failure (permission denied, path conflicts, open_basedir restriction, disk full) into a yii\\base\\Exception carrying the OS message. The is_dir() re-check exists because of yii2 issue #9288: if a concurrent process created the directory between mkdir() failing and the check, the error is tolerated instead of thrown.","triggerScenarios":"Calling FileHelper::createDirectory($path, $mode, $recursive) when: the parent directory is not writable by the PHP process user; $path already exists as a regular file; $path lies outside an open_basedir restriction; $recursive is false and the parent chain is missing; the disk or inode table is full; or an error handler (e.g. Yii's own, or a custom set_error_handler) converts mkdir()'s E_WARNING into an ErrorException, which is what makes the catch block reachable at all.","commonSituations":"Deployments where runtime/cache/assets dirs (e.g. @runtime, @webroot/assets) are owned by the deploy/CLI user but the site runs as www-data or php-fpm; shared hosting with open_basedir; Docker containers with read-only or wrongly-owned volume mounts; a stale file occupying a directory path after a broken deploy; permission modes reset by umask or by rsync without -p.","solutions":["Check the OS message in the exception, then fix ownership/permissions of the parent: chown -R www-data:www-data <parent> && chmod -R 775 <parent> so the PHP process user can write.","Verify $path is not already taken by a regular file (file_exists($path) && !is_dir($path)); remove the file or correct the path/alias (e.g. @runtime/cache vs @runtime/cache/x).","If the path is outside the allowed tree, adjust open_basedir in php.ini/fpm pool config, or move the target inside an allowed alias such as @runtime.","Pass $recursive = true (the default in FileHelper::createDirectory is false, unlike the native mkdir wrapper) when intermediate directories may be missing.","In containers, confirm the volume is mounted rw and the container user matches the volume owner; docker-compose down/up after fixing the host-side ownership."],"exampleFix":"// before\nFileHelper::createDirectory(Yii::getAlias('@runtime'), 0777); // parent owned by root -> mkdir() warns -> exception\n\n// after (host): give the PHP user write access to the parent\n// sudo chown -R www-data:www-data /var/www/app/runtime && sudo chmod -R 775 /var/www/app/runtime\nFileHelper::createDirectory(Yii::getAlias('@runtime'), 0775);","handlingStrategy":"validation","validationCode":"$path = Yii::getAlias('@runtime/cache');\n$parent = dirname($path);\nif (file_exists($path) && !is_dir($path)) {\n    throw new RuntimeException(\"Path is occupied by a regular file: $path\");\n}\nif (!is_dir($parent) && !is_writable(dirname($parent)) === false && !is_dir($parent)) {\n    // rely on recursive creation only when the nearest existing ancestor is writable\n}\n$nearest = $parent;\nwhile (!is_dir($nearest)) { $nearest = dirname($nearest); }\nif (!is_writable($nearest)) {\n    throw new RuntimeException(\"Cannot create directories under $nearest: not writable by \" . get_current_user());\n}\nFileHelper::createDirectory($path, 0775, true);","typeGuard":"/** @psalm-assert !null *//* narrow to a path that can actually be created */\nfunction canCreateDirectory(string $path): bool\n{\n    if (file_exists($path)) {\n        return is_dir($path); // existing dir is fine; existing file is not\n    }\n    $nearest = dirname($path);\n    while (!is_dir($nearest)) {\n        $nearest = dirname($nearest);\n    }\n    return is_writable($nearest);\n}","tryCatchPattern":"try {\n    if (!FileHelper::createDirectory($path, 0775, true) && !is_dir($path)) {\n        throw new RuntimeException(\"mkdir returned false for $path\");\n    }\n} catch (\\yii\\base\\Exception $e) {\n    if (is_dir($path)) {\n        Yii::warning(\"Directory appeared concurrently: {$e->getMessage()}\", __METHOD__);\n    } else {\n        Yii::error(\"Cannot create $path: {$e->getMessage()}\", __METHOD__);\n        throw $e; // surface the real cause (perms / open_basedir) to ops\n    }\n}","preventionTips":["Provision runtime/cache/assets directories in deployment (Ansible, Dockerfile) with the web user as owner instead of creating them on first request.","After every deploy, run a permissions check: the nearest existing ancestor of every runtime path must be writable by the PHP-FPM user.","Always expand path aliases (Yii::getAlias) before calling createDirectory so you log the real filesystem path, not '@runtime/...'.","Pass $recursive = true whenever the parent chain may be missing; the default false surprises ex-Symfony users.","Include is_dir($path) in monitoring: an existing directory proves the operation already succeeded (the #9288 race) and needs no retry."],"tags":["filesystem","permissions","mkdir","php","yii2","deployment"],"backgroundTag":"mkdir-permission-denied","analyzedSha":"66f00d18a29b520f85e8e8f1e32d1e7e7b556cac","analyzedAt":"2026-08-17T05:17:23.470Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}