da-vinci-studio/path-generator
Generate consistent file and directory paths in your Laravel app with configurable patterns and helpers. Useful for organizing uploads, storage, and assets by date, model, or custom rules, keeping paths predictable and easy to change later.
Installation:
composer require da-vinci-studio/path-generator
Add to composer.json if not auto-loaded:
"autoload": {
"psr-4": {
"App\\": "app/",
"DavinciStudio\\PathGenerator\\": "vendor/da-vinci-studio/path-generator/src/"
}
}
Run composer dump-autoload.
Basic Usage:
use DavinciStudio\PathGenerator\PathGenerator;
$generator = new PathGenerator();
$path = $generator->generate('app/storage/logs/{filename}.log');
Replace {filename} with a dynamic value:
$path = $generator->generate('app/storage/logs/{filename}.log', ['filename' => 'app']);
// Output: "app/storage/logs/app.log"
First Use Case: Generate filesystem paths dynamically for:
app/storage/uploads/{user_id}/{filename}.{ext})storage/logs/{service}.{date}.log)config/{env}.php)Dynamic Path Generation:
// Generate a path with placeholders
$path = $generator->generate('app/storage/{type}/{id}/{filename}.{ext}', [
'type' => 'uploads',
'id' => 123,
'filename' => 'profile',
'ext' => 'png'
]);
// Output: "app/storage/uploads/123/profile.png"
Integration with Laravel:
Service Provider Binding:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(PathGenerator::class, function ($app) {
return new PathGenerator();
});
}
Inject via constructor:
public function __construct(private PathGenerator $pathGenerator) {}
Helper Function:
// app/Helpers/path.php
if (!function_exists('generate_path')) {
function generate_path(string $template, array $data = []): string
{
return app(PathGenerator::class)->generate($template, $data);
}
}
Usage:
$path = generate_path('app/storage/{type}/{id}.json', ['type' => 'cache', 'id' => 1]);
Path Validation:
// Check if path is valid before generating files
if ($generator->isValid('app/storage/{type}/{id}', ['type' => 'uploads', 'id' => 123])) {
$path = $generator->generate('app/storage/{type}/{id}', ['type' => 'uploads', 'id' => 123]);
Storage::put($path, $content);
}
Environment-Specific Paths:
$path = $generator->generate(
env('APP_ENV') === 'production'
? 'app/storage/prod/{filename}.log'
: 'app/storage/dev/{filename}.log',
['filename' => 'debug']
);
Path Templates as Config:
Store templates in config/path_templates.php:
return [
'uploads' => 'app/storage/uploads/{user_id}/{filename}.{ext}',
'logs' => 'storage/logs/{service}.{date}.log',
];
Usage:
$template = config('path_templates.uploads');
$path = $generator->generate($template, ['user_id' => 1, 'filename' => 'avatar', 'ext' => 'jpg']);
Path Generation in Controllers:
public function store(Request $request)
{
$request->validate(['file' => 'required|file']);
$file = $request->file('file');
$path = $this->pathGenerator->generate(
'app/storage/uploads/{user_id}/{filename}.{ext}',
[
'user_id' => auth()->id(),
'filename' => Str::slug($file->getClientOriginalName()),
'ext' => $file->getClientOriginalExtension(),
]
);
$file->storeAs('public', basename($path));
}
Path Generation in Models:
class Upload extends Model
{
public function getPathAttribute(): string
{
return app(PathGenerator::class)->generate(
'app/storage/uploads/{user_id}/{filename}.{ext}',
[
'user_id' => $this->user_id,
'filename' => $this->filename,
'ext' => $this->extension,
]
);
}
}
Path Generation in Blade:
// Add to Composer autoload (if not already)
// composer.json: "autoload": { "files": ["app/Helpers/path.php"] }
Blade usage:
<img src="{{ generate_path('app/storage/uploads/{user_id}/{filename}.{ext}', [
'user_id' => $user->id,
'filename' => $upload->filename,
'ext' => $upload->extension
]) }}">
Placeholder Case Sensitivity:
{Filename} ≠ {filename}.Missing Placeholders:
{filename}).$requiredPlaceholders = ['filename', 'ext'];
$missing = array_diff($requiredPlaceholders, array_keys($data));
if (!empty($missing)) {
throw new \InvalidArgumentException("Missing placeholders: " . implode(', ', $missing));
}
Path Traversal Risks:
../ to escape directories.realpath() to resolve paths:
$path = $generator->generate($template, $data);
$resolvedPath = realpath($path);
if ($resolvedPath === false || strpos($resolvedPath, $path) !== 0) {
throw new \RuntimeException("Invalid path resolution");
}
Deprecated Methods:
Enable Debug Output:
$generator->setDebug(true);
$path = $generator->generate('app/{type}/{id}', ['type' => 'storage', 'id' => 1]);
// Outputs: "Replaced placeholders: ['type' => 'storage', 'id' => '1']"
Log Generated Paths:
$path = $generator->generate($template, $data);
\Log::debug('Generated path', ['template' => $template, 'data' => $data, 'path' => $path]);
Test Edge Cases:
$generator->generate('app/{type}', []); // Output: "app/{type}"
$generator->generate('app/{folder}/{subfolder}/{filename}', [
'folder' => 'storage/{type}',
'subfolder' => 'uploads',
'filename' => 'file.txt',
'type' => 'user'
]);
// Output: "app/storage/user/uploads/file.txt"
Note: Nested placeholders are resolved literally (not recursively).Custom Placeholder Handlers: Override placeholder logic by extending the class:
class CustomPathGenerator extends \DavinciStudio\PathGenerator\PathGenerator
{
protected function replacePlaceholder(string $placeholder, string $value): string
{
// Custom logic (e.g., URL-encode values)
return str_replace(' ', '-', $value);
}
}
Add Path Validation Rules: Extend to validate paths against a whitelist:
class ValidatedPathGenerator extends \DavinciStudio\PathGenerator\PathGenerator
{
private $allowedPaths = ['app/storage/uploads', 'app/storage/logs'];
public function generate(string $template, array $data = []): string
{
$path = parent::generate($template, $data);
$resolvedPath = realpath($path);
$basePath = realpath(dirname($path));
foreach ($this->allowedPaths as $allowed) {
if (strpos($resolvedPath, $allowed) ===
How can I help you explore Laravel packages today?