Changing the public folder in Laravel 5

I have been playing around with Laravel 5 recently and it's really looking great. Definitely different, and definitely improved.

First things first, I strive to setup my laravel projects with 2 main folders, separating the Public facing website and assets from the Private app, API, and configuration.

In Laravel 4

In L4, this was pretty simple:

Move the application files

  1. Create application_files folder
  2. Move all application folders and files into it
  3. Edit index.php and adjust the paths to include /application_files
/*
|-------------------------------------------------------------------
| Register The Auto Loader
|-------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/

require __DIR__.'/../application_files/bootstrap/autoload.php';

/*
|--------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------
|
| We need to illuminate PHP development, so let's turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight these users.
|
*/

$app = require_once __DIR__.'/../application_files/bootstrap/start.php';

Change the public folder

  1. Rename the folder
  2. Edit /bootstrap/paths.php and adjust the folder name to what you called it
/*
 |-------------------------------------------------------------------
 | Public Path
 |-------------------------------------------------------------------
 |
 | The public path contains the assets for your web application, such as
 | your JavaScript and CSS files, and also contains the primary entry
 | point for web requests into these applications from the outside.
 |
 */

 'public' => __DIR__.'/../public_html',

Enter Laravel 5

Luckily, in Laravel 5, moving the application files and folders is exactly the same, no problems there.

However, /bootstrap/paths.php has been removed, and there isn't a readily obvious place where the public path is set.

After doing some digging, I found this Laracasts.com Forum post where this is discussed thoroughly. Yay! Solution!


(The following is referencing @cbj4074's findings in the laracasts.com forum thread. He really knows his stuff!)

However, after looking closer, the chosen "Best Answer" doesn't appear to be the best answer, because the solution needs to be twofold:

  1. be able to detect environment and respond accordingly
  2. needs to update the public_path() helper function in the context of configuration files so that the public path can be defined correctly in many third-party packages.

So here is @cbj4074's solution (paraphrased for brevity).

@cbj4074's Research

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. 2.) Using IoC container in /app/Providers/AppServiceProvider.php This is not a bad approach, but 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. 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'. 4.) Using IoC container in /index.php Given that /public/index.php already requires modifications to function out-of-the-box on many systems (due to realpath() not being used in the require and requireonce statements causing the application to fail fatally in environments in which PHP enforces openbasedir restrictions), 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.


@cbj4074's Solution

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.

This final solution covers all concerns, being changeable based on environment, and overriding the public_path() so that third-party packages don't fail.

Implementation

Here's how I, personally, have implemented his solution:

1). Edit the public/index.php file to include a new public_path() function definition, thereby overriding the default function

/*
|--------------------------------------------------------------------
| public_path() function
|--------------------------------------------------------------------
|
| 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.
|
| @cbj4074 - https://laracasts.com/discuss/channels/general-discussion/where-do-you-set-public-directory-laravel-5
*/

function public_path($path = '')
{
    return realpath(__DIR__)
        .($path ? DIRECTORY_SEPARATOR.$path : $path);
}

2). Next, create a custom service provider with artisan (at this point, it will need run in the application_files directory where artisan is residing.

$ cd application_files/ $ php artisan make:provider PublicPathServiceProvider

3.) Edit the register() function in the newly created /app/Providers/PublicPathServiceProvider.php

public function register()
{
  if (env('PUBLIC_PATH') !== NULL) {

    //An example that demonstrates setting Laravel's public path.
    $this->app['path.public'] = base_path().'/../'.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'));
    */
  }
  else
  {
    $this->app['path.public'] = base_path().'/../public_html';
  }

  // Possible environment changes
  if ($this->app->environment() === 'local') {

  }
  elseif ($this->app->environment() === 'test') {

  }
  elseif ($this->app->environment() === 'production') {

  }
}

4.) And don't forget to register the PublicPathServiceProvider in config/app.php so that your application will actually run the service provider.

/*
 * Application Service Providers...
 */
App\Providers\AppServiceProvider::class,
App\Providers\EventServiceProvider::class,
...
App\Providers\PublicPathServiceProvider::class,

Conclusion

And there you have it, now PUBLIC_PATH can be set in your .env file if you don't want the public folder to be public_html and all of the application files and folders are nicely tucked away as not to get in anyones way when the project is deployed.

Notes

I did keep these files/folders outside of the application_files directory in L5:

  • .git/
  • .gitignore
  • .gitattributes
  • gulpfile.js
  • package.json
  • readme.md

I also changed the .gitignore to exclude files/folders within the application_files directory:

/application_files/vendor
/application_files/composer.lock
/node_modules
Homestead.yaml
/application_files/.env

-- Update --

Laravel Elixir

I also had to add these variables in the elixir gulpfile.js so that it worked correctly.

/*
 |--------------------------------------------------------------------
 | Elixir Configuration
 |--------------------------------------------------------------------
 */

elixir.config.assetsPath = 'application_files/resources/assets/';
elixir.config.appPath = 'application_files/app';
elixir.config.publicPath = 'public_html';

Works like a charm!

Josh Friend

Full-Stack Software Developer at Ramsey Solutions. I build mostly using Ruby on Rails, React, Laravel, and Magnolia CMS. I always wanted to be an inventor when I grew up, so now I enjoy providing simple, intuitive solutions to complex problems.

Nashville, Tennessee

Subscribe to joshwhatk

Get the latest posts delivered right to your inbox.