{"record":{"id":"fc6120ad0a1913bd","repo":"Leantime/leantime","slug":"notifications-plugin-install-cant-find-composer","errorCode":null,"errorMessage":"notifications.plugin_install_cant_find_composer","messagePattern":"notifications\\.plugin_install_cant_find_composer","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"app/Domain/Plugins/Services/Plugins.php","lineNumber":267,"sourceCode":"\n                    return null;\n                }\n            })\n            ->filter()->all();\n\n        return $newPlugins;\n    }\n\n    public function createPluginFromComposer(string $pluginFolder, string $license_key = ''): InstalledPlugin\n    {\n        $pluginPath = Str::finish($this->pluginDirectory, DIRECTORY_SEPARATOR).Str::finish($pluginFolder, DIRECTORY_SEPARATOR);\n\n        if (file_exists($composerPath = $pluginPath.'composer.json')) {\n            $format = 'folder';\n        } elseif (file_exists($composerPath = \"phar://{$pluginPath}{$pluginFolder}.phar\".DIRECTORY_SEPARATOR.'composer.json')) {\n            $format = 'phar';\n        } else {\n            throw new \\Exception(__('notifications.plugin_install_cant_find_composer'));\n        }\n\n        $json = file_get_contents($composerPath);\n        $pluginFile = json_decode($json, true);\n\n        $plugin = build(new InstalledPlugin)\n            ->set('name', $pluginFile['name'])\n            ->set('enabled', 0)\n            ->set('description', $pluginFile['description'])\n            ->set('version', $pluginFile['version'])\n            ->set('installdate', date('y-m-d'))\n            ->set('foldername', $pluginFolder)\n            ->set('license', $license_key)\n            ->set('format', $format)\n            ->set('homepage', $pluginFile['homepage'])\n            ->set('authors', json_encode($pluginFile['authors']))\n            ->get();\n","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/Leantime/leantime/blob/9a9f49f1008f4782b30f6723c54228f4f992e636/app/Domain/Plugins/Services/Plugins.php#L249-L285","documentation":"Plugins::createPluginFromComposer() builds an InstalledPlugin from a plugin on disk. It looks for composer.json in two places only: app/Plugins/{folder}/composer.json (folder format) or phar://app/Plugins/{folder}/{folder}.phar/composer.json (phar format - note the folder name is repeated inside the phar:// URL). When neither path exists it throws the translated 'Could not find composer.json' error. Every Leantime plugin, folder-based or marketplace phar, must carry a composer.json with name, description, and version at one of those exact locations.","triggerScenarios":"Calling createPluginFromComposer($folder): (1) the folder does not exist under app/Plugins at all; (2) a zip was extracted with an extra nesting level (MyPlugin/MyPlugin/composer.json); (3) a phar plugin whose inner file is not named {folder}.phar; (4) app/Plugins is the private git submodule and was never initialized, so the tree is empty; (5) case mismatch between the Studly-cased folder name generated from the download filename and the actual directory.","commonSituations":"Fresh clones of the OSS repo where app/Plugins is an uninitialized submodule ('git submodule update --init' missing); manual plugin uploads where the zip contains a wrapper directory; marketplace installs whose Content-Disposition filename produced a foldername that differs from the phar's internal name; renaming plugin folders by hand after install.","solutions":["ls app/Plugins/<folder> and confirm composer.json sits directly at the folder root (not one level deeper).","If app/Plugins is empty in a git checkout, run git submodule update --init --recursive.","Re-extract the plugin zip so composer.json is at app/Plugins/<folder>/composer.json, removing any nested wrapper directory.","For phar plugins, ensure the file inside is named <folder>.phar (the code appends the folder name inside the phar:// URL).","Verify the folder name string passed in matches the on-disk directory exactly, including casing."],"exampleFix":"; before: extraction produced a nested wrapper - lookup fails\napp/Plugins/\n└── MyPlugin/\n    └── MyPlugin/\n        └── composer.json\n\n; after: composer.json at the root of the folder the foldername points at\napp/Plugins/\n└── MyPlugin/\n    └── composer.json","handlingStrategy":"validation","validationCode":"$base = rtrim(app/Plugins path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR;\n$folderOk = is_file($base.'composer.json');\n$pharOk = is_file(\"phar://{$base}{$folder}.phar\".DIRECTORY_SEPARATOR.'composer.json');\nif (! $folderOk && ! $pharOk) {\n    throw new InvalidArgumentException(\"Not a valid plugin folder '{$folder}': no composer.json in folder or phar layout\");\n}","typeGuard":"/** True when $folder is an installable plugin (composer.json in folder or phar layout). */\nfunction isValidPluginFolder(string $pluginsDir, string $folder): bool\n{\n    $base = rtrim($pluginsDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR;\n\n    return is_file($base.'composer.json')\n        || is_file(\"phar://{$base}{$folder}.phar/composer.json\");\n}","tryCatchPattern":"try {\n    $plugin = $plugins->createPluginFromComposer($folder);\n} catch (\\Exception $e) {\n    // message is the localized 'Could not find composer.json'\n    Log::warning(\"Plugin install aborted for '{$folder}': {$e->getMessage()}\");\n    return back()->with('error', $e->getMessage());\n}","preventionTips":["Run git submodule update --init --recursive after cloning so app/Plugins is populated.","Validate uploaded plugin zips extract with composer.json at the folder root (no wrapper directory).","Keep the on-disk folder name identical to the name derived from the download filename.","For phar plugins, name the inner file <folder>.phar to match the expected phar:// layout."],"tags":["plugins","composer","filesystem","installation"],"backgroundTag":"missing-composer-json","analyzedSha":"9a9f49f1008f4782b30f6723c54228f4f992e636","analyzedAt":"2026-08-21T02:37:38.966Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}