konsulting/project-root
Resolve the correct root path when developing a Composer package or using it as a dependency. Project Root lets you target a package name and resolve paths relative to the host project, avoiding repeated “dirty” path-detection logic.
Install the Package:
composer require konsulting/project-root
Basic Usage:
Replace fragile path logic (e.g., dirname(__DIR__, 4)) with:
use Konsulting\ProjectRoot;
$projectRoot = ProjectRoot::forPackage('your-package-name')->resolve(__DIR__);
Where to Look First:
/tests/ for edge cases (e.g., symlinked dependencies)./src/ProjectRoot.php (50 lines) to understand the resolution logic.First Practical Use Case:
A Laravel package that needs to write logs to the project’s storage/logs/ directory (not its own):
$logPath = ProjectRoot::forPackage('your-package')->resolve(__DIR__ . '/../../../storage/logs');
Replace hardcoded paths in packages:
// Before (fragile)
$configPath = __DIR__ . '/../../../../config/package.php';
// After (robust)
$configPath = ProjectRoot::forPackage('your-package')->resolve(__DIR__ . '/config/package.php');
Resolve project roots in Artisan commands or standalone scripts:
use Konsulting\ProjectRoot;
$projectRoot = ProjectRoot::forPackage('your-cli-package')->resolve(__DIR__);
$this->info("Project root: " . $projectRoot);
Dynamically load project-specific configs:
public function boot()
{
$projectRoot = ProjectRoot::forPackage('your-service-provider')->resolve(__DIR__);
$this->app->singleton('projectConfig', function () use ($projectRoot) {
return require $projectRoot . '/config/project.php';
});
}
Locate project assets (e.g., public/ or resources/) from a package:
$publicPath = ProjectRoot::forPackage('your-asset-package')->resolve(__DIR__ . '/../../../public');
Avoid Mixing with Laravel Helpers:
Use ProjectRoot for package-relative paths (e.g., vendor/package/storage).
Reserve Laravel’s storage_path(), public_path() for project-wide paths.
Package Bootstrapping:
Resolve paths in register() (not boot()) to ensure they’re available early:
public function register()
{
$packageRoot = ProjectRoot::forPackage('your-package')->resolve(__DIR__);
$this->mergeConfigFrom($packageRoot . '/config/package.php', 'package');
}
static private $resolvedPaths = [];
$projectRoot = self::$resolvedPaths[$packageName] ?? ProjectRoot::forPackage($packageName)->resolve(__DIR__);
self::$resolvedPaths[$packageName] = $projectRoot;
Unit Test Path Resolution:
Mock __DIR__ to simulate different contexts:
$mockDir = __DIR__ . '/../../../vendor/your-package';
$resolver = ProjectRoot::forPackage('your-package')->resolve($mockDir);
$this->assertEquals('/path/to/project', $resolver);
Integration Test in CI:
Test with --prefer-source and --prefer-dist to cover symlinked vs. vendored scenarios:
composer install --prefer-dist && vendor/bin/phpunit
composer install --prefer-source && vendor/bin/phpunit
forPackage('wrong-name') returns incorrect paths.composer.json:
$composerJson = json_decode(file_get_contents(__DIR__ . '/../../composer.json'), true);
if ($composerJson['name'] !== 'your-package') {
throw new \RuntimeException("Package name mismatch!");
}
realpath() may resolve symlinks incorrectly on Windows/Linux.$path = str_replace('\\', '/', $path);
$path = preg_replace('/\/+/', '/', $path);
composer dump-autoload --optimize
DIRECTORY_SEPARATOR may not work as expected in cross-platform packages.$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
vendor/a/b/c/your-package) may break resolution.composer show -v your-package to debug the actual install path.Check the resolved path matches expectations:
$resolved = ProjectRoot::forPackage('your-package')->resolve(__DIR__);
file_put_contents('/tmp/debug-path.txt', $resolved);
Inspect how Composer resolves the package:
composer show -v your-package
Look for Installed Path to confirm the expected directory.
Add a fallback for critical paths:
try {
$path = ProjectRoot::forPackage('your-package')->resolve(__DIR__ . '/config');
} catch (\Exception $e) {
$path = __DIR__ . '/config'; // Fallback to package root
\Log::warning("Project root resolution failed: " . $e->getMessage());
}
The package is zero-config. No .env or service provider setup is needed.
composer.jsonThe argument to forPackage() must match the name field in composer.json:
{
"name": "vendor/your-package",
"require": { ... }
}
Use the full namespace (e.g., vendor/your-package, not just your-package).
resolve()__DIR__ or an absolute path to resolve().
// Correct (relative to package root)
$path = ProjectRoot::forPackage('your-package')->resolve(__DIR__ . '/config');
// Incorrect (absolute path may bypass resolution)
$path = ProjectRoot::forPackage('your-package')->resolve('/absolute/path');
Extend the package by subclassing ProjectRoot:
class CustomProjectRoot extends \Konsulting\ProjectRoot
{
public function resolve($path, $fallback = null)
{
$resolved = parent::resolve($path);
return $resolved ?: $fallback;
}
}
Cache resolved paths globally:
class CachedProjectRoot extends \Konsulting\ProjectRoot
{
static private $cache = [];
public function resolve($path)
{
$package = $this->packageName;
return self::$cache[$package] ?? (self::$cache[$package] = parent::resolve($path));
}
}
Extend to handle monorepos or custom Composer repos:
class MultiRootProjectRoot extends \Konsulting\ProjectRoot
{
public function resolve($path, $root = null)
{
if ($root) {
return $root . '/' . ltrim($path, '/');
}
return parent::resolve($path);
}
}
ProjectRoot for project-wide paths (e.g., storage_path()).ProjectRoot for package-specific paths (e.g., vendor/package/storage).How can I help you explore Laravel packages today?