As a new Laravel user, I find it staggering that there is no simple, elegant, centralized, and environment-aware means by which to define both the "private" and the "public" filesystem paths that are referenced throughout the application.
While it may make sense to nest the public directory beneath the application root for source-control and packaging/distribution purposes, that is an unrealistic structure in a real-world deployment scenario.
The scenario that the OP describes is far more realistic, wherein the "public" directory is actually the web-server's "document root", and the rest of the application resides above and outside of the document root.
The default directory structure inspires less capable users to dump the entire application (including everything outside of the public directory) into the web-server's document root. This practice should be discouraged, as it exposes a broad attack-vector: the ability to access application resources via HTTP.
One might argue that a properly-configured server does not expose the application to unnecessary risk when Laravel is deployed as-is, but there is no guarantee as to the environment's viability. If PHP is upgraded and the process goes awry, thereby causing the web-server to present .php files as raw file downloads, an active attacker could conceivably "scrape" most or all source code. This type of mis-configuration has the potential to expose application details, ranging from filesystem paths to various credentials (cringe).
My research on this subject has yielded five possible approaches to using a more sensible "public" path (and I'm sure there are others):
1.)
Using a symbolic link (or junction point, on NTFS volumes)
This method is not portable. While it may be an acceptable solution for a solo developer, in a collaborative development environment, it's a non-option because it doesn't travel with the application source-code and is therefore cumbersome to implement consistently.
2.)
Using IoC container in /app/Providers/AppServiceProvider.php
This is not a bad approach, but as @patrickbrouwers notes, it suffers from a considerable flaw: the public_path() helper function remains unaffected in the context of configuration files, which causes the public path to be defined incorrectly in many third-party packages.
Also, if I were going to implement this approach, I would create a new Service Provider, rather than modify the included AppServiceProvider class (only because doing so reduces the likelihood of having to merge-in upstream changes).
3.)
Using IoC container in /bootstrap/app.php
This works well enough -- until environment-detection via the .env file is necessary. Any attempt to access $app->environment() at this stage yields, Uncaught exception 'ReflectionException' with message 'Class env does not exist'. Furthermore, in order to minimize the impact of upstream changes, my preference is to limit customization to as few files as possible, which brings me to the next method.
4.)
Using IoC container in /index.php
Given that /public/index.php already requires modifications to function out-of-the-box on many systems (the fact that realpath() is not used in the require and require_once statements causes the application to fail fatally in environments in which PHP enforces open_basedir restrictions, which do not allow nor resolve relative path notations, such as ../), my preference would be to make this change (to the public path definition) in the same file.
But, as with the above method, attempts to perform environment-detection cause a fatal error in this context, because the required resources have not yet been loaded.
5.)
Using custom IoC container in /app/Providers/*.php, coupled with overriding public_path() helper function
I settled on this method because it is the only method that solves the third-party package configuration problem (mentioned in method #2) and accounts for environment-detection.
The public_path() helper function (defined in /vendor/laravel/framework/src/Illuminate/Foundation/helpers.php) is wrapped in if ( ! function_exists('public_path')), so, if a function by the same name is defined before this instance is referenced, it becomes possible to override its functionality.
Given that I have already had to modify index.php (due to the open_basedir problems that I explained in #4), I elected to make this change in index.php, too.
//Defining this function here causes the helper function by the same name to be
//skipped, thereby allowing its functionality to be overridden. This is
//required to use a "non-standard" location for Laravel's "public" directory.
function public_path($path = '')
{
return realpath(__DIR__);
}
Next, I created a custom Service Provider (instead of using AppServiceProvider.php, the reasons for which I explain in #2):
php artisan make:provider MyCustomServiceProvider
The file is created at /app/Providers/MyCustomServiceProvider.php. The register() method can then be populated with something like the following:
/**
* Register the application services.
*
* @return void
*/
public function register()
{
if (env('PUBLIC_PATH') !== NULL) {
//An example that demonstrates setting Laravel's public path.
$this->app['path.public'] = env('PUBLIC_PATH');
//An example that demonstrates setting a third-party config value.
$this->app['config']->set('cartalyst.themes.paths', array(env('PUBLIC_PATH') . DIRECTORY_SEPARATOR . 'themes'));
}
//An example that demonstrates environment-detection.
if ($this->app->environment() === 'local') {
}
elseif ($this->app->environment() === 'development') {
}
elseif ($this->app->environment() === 'test') {
}
elseif ($this->app->environment() === 'production') {
}
}
At this point, everything seems to be working over HTTP. And while all of the artisan commands that I've tried have worked as expected, I have not exhausted all possible CLI scenarios.
I welcome any comments, improvements, or other feedback regarding this approach.
Thanks to everyone above for contributing to this solution!