Jan 16, 2015 | laravel, laravel 5

Upgrading from Laravel 4 to Laravel 5

Series

This is a series of posts on New Features in Laravel 5.0.

!
Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

It's very simple to get started in a new Laravel 5 app: composer create-project laravel/laravel my-project-name-here dev-develop --prefer-dist. But what if you have a Laravel 4 app you want to upgrade?

You might think the answer is to upgrade the Composer dependencies and then manually make the changes. Quite a few folks have created walkthroughs for that process, and it's possible—but there are a lot of little pieces you need to catch, and Taylor has said publicly that he thinks the better process is actually to start from scratch and copy your code in. So, that's what we're going to be doing.

This process took me 3 hours the first time (because I was writing this article), and 1 hour the second time. SaveMyProposals isn't hugely complex, but hopefully this guide will keep your upgrade time low.

Getting Started

So, we’re working on upgrading SaveMyProposals, in my local ~/Sites/savemyproposals directory.

We want to have a copy of the new site (a blank laravel 5 install) and the old site (the Laravel 4.2 savemyproposals from the github repo) next to each other, so we’ll do an additional clone of the savemyproposals repo into a parallel directory l5smp. I’m going to do the Laravel 5 upgrade in my NEW directory, not in my previous working directory, so that it’s easier to make sure I don’t lose any git-ignored configuration files in the process.

cd ~/Sites
git clone git@github.com:mattstauffer/savemyproposals.git l5smp
cd l5smp
git checkout -b features/laravel-5

OK, so now let’s clean out our laravel-5 install. We want to delete everything; however, we can’t delete .git, or we’d be deleting our git repo entirely. So, here’s the fastest way I came up with, but I’d love someone to chip in if there’s a cleaner solution:

cd ~/Sites/l5smp
rm -rf *
rm .gitattributes
rm .gitignore

Also, if you have any other files in your home directory left over after this—e.g. .scrutinizer.yml—delete those too. We’ll be copying them over in a later step. You want nothing in your directory except .git.

That’s it. We now have a clean install. Let’s get Laravel 5 in there! If you’re like me, you’ll want a clean point you can revert back to, so I actually committed here:

git commit -am "Delete everything in preparation for Laravel 5."

NOTE: If you commit this delete, you’ll be losing the continuity of the history of any files that you plan to bring back later. However, we can use git squash to merge this commit in later, which will bring that continuity back.

OK, let's get the Laravel files in there. Thankfully, Isern Palaus (@ipalaus) got me a very simple version of this step.

git remote add laravel https://github.com/laravel/laravel.git
git fetch laravel
git merge laravel/master --squash
git add .
git commit -am "Bring in Laravel 5 base."
composer install

You should be able to check to make sure this app works (without anything in it) by running the following:

php -S localhost:8090 -t public/

And visiting http://localhost:8090/ in your browser. If you see this, you’re doing good:

Laravel Start Screen

Now, to bring everything back. What I did was open the directories up in side-by-side panels in iTerm 2 and just start listing out the files in my old site (~/Sites/savemyproposals) and moving them into the right places in the new site (~/Sites/l5smp). Here are the steps; I’ll be referring to OLD and NEW, OLD being the directory for my laravel 4 code and NEW being the new blank Laravel 5 install.

PSR-4 namespace

If you already have a top-level PSR-0/PSR-4 namespace set up, like I did for SaveMyProposals, you’ll want to use the new app:name Artisan command.

php artisan app:name SaveMyProposals

Your domain folder

If you follow the common community practice of having a folder either at the top level or in app with the name of your top-level namespace—e.g. I have app/SaveMyProposals that is my PSR-0 source.

The easiest way to map this is just to move all of the folders under this folder into your app folder, and it'll just bring them into your already-established namespace. Done.

Composer.json dependencies

Work through your composer.json in OLD, praying as you go that all of your packages have been updated for Laravel 5, and move your dependencies and other customizations into NEW’s composer.json.

Now, try a composer update to see what you get.

The app/ Directory

Commands

Move from app/commands => app/Console/Commands

Either namespace your commands, or add them to the composer.json classmap autoloader.

Note that, in Laravel 5, the default Inspire Command comes with a handle() method, but Laravel will call either fire() (old style) or handle() (new style), whichever it finds.

Move the bindings of your commands from start/artisan.php into app/Console/Kernel.php, where the $commands array lists out all of the commands to register.

Config

See Configuration below

Controllers

Move from app/controllers => app/Http/Controllers

Either namespace your controllers (directions below) or drop the namespace from the abstract app/Http/Controllers/Controller.php and autoload the app/Http/Controllers directory via composer classmap.

Database Migrations & Seeds

Move from app/database => database

Delete the 2014_10_12_00000_create_users_table, since you should already have this (although you should make sure that you have the remember_token field, which was added in 4.1.26). You can keep the password_reset migration--that's new in Laravel 5.

Filters

Laravel 5 has moved to focusing on Middleware for things we used to use filters for, but you can still port your old filters over. Just open up app/Providers/RouteServiceProvider.php and paste your bindings into boot(). E.g.

// app/filters.php
Router::filter('shall-not-pass', function() {
    return Redirect::to('shadow');
});

could be moved in like this:

// app/Providers/RouteServiceProvider@boot()
$router->filter('shall-not-pass', function() {
    return \Redirect::to('shadow');
});

Note that you don't need to move over any of the filters that come in by default; they're all here, but now as Middleware.

Language files

Move from app/lang => resources/lang

Models

Laravel 5—and most of the advice from the community for quite some time—has done away with the concept of a models folder. But if your old app uses it, just create a models directory within app and classmap autoload it (by adding it to composer.json’s classmap autoload section):

"autoload": {
    "classmap": [
        "database",
        "app/models"
    ]
}

Note that the User.php that comes with Laravel 5 lives in the app directory, so you could also place your model files there and put them in your top level namespace (e.g. SaveMyProposals/Conference for the Conference model).

Soft Deletes

If you use the SoftDeletingTrait on any of your models, you'll want to rename the trait to SoftDeletes.

Routes

Move app/routes.php => app/Http/routes.php

Adjust any routes that use the built-in routes from, for example, before => auth to middleware => auth.

Start

artisan.php
See notes above about moving command bindings.

global.php
Global.php is a catchall for many people. Anything in here should likely be added to a Service Provider; but if not, you can register bindings in SaveMyProposals\Providers\AppServiceProvider in the register() method.

Tests

Move from app/tests => tests

Views

Move from app/views => resources/views

Namespacing controllers, commands, etc.

Namespacing controllers

If your controllers weren't namespaced in the old codebase, you can either bring them in with no namespace, or add namespaces to them.

If you want to add namespaces, just go to each controller and add SaveMyProposals\Http\Controllers as their namespace.

If you want to go without, edit app/Providers/RouteServiceProvider.php and set protected $namespace equal to null. Then add the controllers directory to classmap in the composer.json autoload section. Then edit the map() method to be just this (replace the entire $this->loadRoutesFrom line):

    include app_path('Http/routes.php');

Note: If you namespace your controllers, all of your internal façade calls will fail; the simpler way is to choose to not namespace controllers. If you do, you'll see something like Class 'SaveMyProposals\Http\Controllers\Auth' not found.. If this happens, you just need to use Auth, use Session, etc. at the top of the controller—or just prepend \ to each inline (e.g. convert Session::flash('stuff', 'other stuff'); to \Session::flash('stuff', 'other stuff');.)

Namespacing Artisan commands

Just like controllers, you can either namespace or tweak the setup. Namespacing Artisan commands works just like with controllers. You can tweak the set up to work with non-namespaces commands by changing the referred-to namespace in app/Console/Kernel.php's $commands property, and then classmap autoloading the app/Console/Commands directory in composer.json.

Bootstrap directory

If you’ve made any customizations to files in bootstrap—and if you made any, it was likely only start.php—you’ll want to move them over. Note that detectEnvironment behaves differently in Laravel 5, so don’t even worry about copying over anything about environment detection. You’re going to be re-doing this.

Public directory

You can delete every file out of NEW’s public directory except index.php, and move your OLD public files in.

You’ll notice that the Laravel 5 app structure puts the source Less files in resources/assets/less, so if you want to follow this convention you can put Sass or Less files and any other sources there. But you don’t have to, so for this walkthrough we won’t.

Loose files in your top-level directory

This is up to you. readme.md, .scrutinizer.yml, .travis.yml, travis.php.ini, package.json, gulpfile.js, whatever. Note that Laravel 5 ships with package.json and gulpfile.js by default, so you’ll want to check those out before you just overwrite them.

Also, be sure to bring in any customization you’ve made to .gitignore, .gitattributes, or phpunit.xml.

Configuration

.env.local.php => .env

I copied .env.local.php from OLD to NEW. I then edited it to turn it from a PHP array into a .env format, from this:

<?php return [
'key' => 'value'
];

to this:

key=value

I also edited .env.example to show what keys I expect in each .env file:

key=valueHere

I also added APP_ENV (set to "local"),APP_DEBUG (set to true), APP_KEY (set to my encryption key), DB_HOST & DB_DATABASE & DB_USERNAME & DB_PASSWORD set to their appropriate values, and CACHE_DRIVER and SESSION_DRIVER set to 'file', as these are used internally in the framework.

Config files

Drop the concept of local vs. production vs. staging config files. Drop the idea of .env.local.php, .env.staging, etc. Configuration file loading, and environment detection, is endlessly simpler.

Every piece of config that's consistent across all installs should live in the very-familiar config files in the config directory.

Every piece of config that's specific to each install should live in .env, which should be git ignored.

.env.example should show all of the fields that should be present in each .env file.

So, copy all of your OLD universal values from the config files into the NEW config directory, and then extract changing values into .env and .env.example, and then use those inline your code using env('KEY_NAME_HERE').

Auth & Users

The fastest trick is just to use the pre-existing User model, but if you can't do that, here's what you want to do:

Delete the following from your use block:

use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;

Add the following to your use block:

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

Remove the UserInterface and RemindableInterface interfaces

If you used them, remove Illuminate\Auth\Reminders\RemindableTrait and Illuminate\Auth\UserTrait from your use block and your class declaration.

Mark it as implementing the following interfaces:

implements AuthenticatableContract, CanResetPasswordContract

Include the following within the class declaration, to use them as traits:

use Authenticatable, CanResetPassword;

And finally, either change the namespace of your User model to your app namespace, or change the 'model' property in config/auth.php to the correct namespace (e.g. User instead of SaveMyProposals\User).

Finally, if you're using your own User model, you can delete app/user.php.

Form & HTML Helpers

Class Form not found

If you're using Form or Html helpers, you'll see an error stating class 'Form' not found (or the same for Html). Just go to Composer and require "illuminate/html": "~5.0".

You'll also need to get the Façade and the service provider working. Edit config/app.php, and add this line to the 'providers' array:

    'Illuminate\Html\HtmlServiceProvider',

And add these lines to the 'aliases' array:

        'Form'      => 'Illuminate\Html\FormFacade',
        'Html'      => 'Illuminate\Html\HtmlFacade',

{{ }} escaping

The best way to handle the change from @{{ to {!! for raw HTML output in Blade is to just use find and replace any time you KNOW you have to have raw output—for example, if you're using Laravel form helpers—and replace @{{ with @{!! and }} with !!} in those contexts. Everywhere else, just leave it as {{ and }}; that should be the default echo syntax from now on.

If for some reason you need to use the old Blade syntax, you can define that. Just add the following lines at the bottom of AppServiceProvider@register():

        \Blade::setRawTags('{{', '}}');
        \Blade::setContentTags('{{{', '}}}');
        \Blade::setEscapedContentTags('{{{', '}}}');

Note that if you change the raw tags this way, your comments with @{{-- will no longer work.

Cleaning up

If you did the commits along the way like I did, you can squash them together to get continuity with git squash. Run git log to see how many commits you used; I used 3. Then run git rebase -i HEAD~3 (replace 3 with your number.)

This will open Git, and you can now squash the commits. If you're unfamiliar with git squash, check out my tutorial on Squashing Git Commits.

Miscellaneous

Façades in namespaced controllers

Because everything's namespaced, all of your controllers' View::make() (and any other façade accessed with your namespaced controllers) will break because it can't the top-level namespaced View, Auth, etc. Probably the simplest solution is to use View at the top of the file, although there are quite a few more architecturally "pure" ways.

Whoops

If you miss the Whoops error handler, I have a post on how you can bring it back.

Packages

Lots has changed in Laravel 5 with how packages work, so it's likely there will be a lot of wrinkles to be ironed out there. If you run into particular issues there, please leave notes in the comments so I can get this section more comprehensive. For now, Ryan Tablada warns: "Be prepared for function "package" does not exist".

What didn't move over?

There are probably plenty of packagers that won't make it. Most Laravel-specific packages won't. In this codebase, bugsnag-laravel was the only such package.

Concluding

This was a quick run-through. I'm confident that I'm missing some pieces here, because I only picked up what happened for this particular site's upgrade. So, in a move counter to my usual policy, I'm going to open up comments on a Github Gist where folks can provide corrections/updates/etc.

That's it! As you can see, there are a lot of pieces, but this is actually a very simple and quick upgrade, considering that we're upgrading major versions of a framework here. Go Forth and Upgrade!

Comments Gist

Troubleshooting

Eloquent

If you see this error:

PHP Fatal error:  Class 'Eloquent' not found in /path/to/YourModel.php

... that means YourModel is extending \Eloquent. To make this work, just add this use to that model's use block:

use Illuminate\Database\Eloquent\Model as Eloquent;

String Given

If you see this error:

Catchable fatal error: Argument 1 passed to Illuminate\Foundation\Application::__construct() must be an instance of Illuminate\Http\Request, string given

... you need to run composer install.

Call to a member function domain() on a non-object

If you see the error:

Call to a member function domain() on a non-object

It means one of your route actions isn't linking correctly. For example, if you're linking to a route named "signup" and you don't have a route with that name, you'll get this error.

More likely in a Laravel 5 upgrade, it has to do with the namespacing or non-namepsacing of your controllers.

Illuminate\Session\TokenMismatchException

If you start seeing Illuminate\Session\TokenMismatchException show up--likely in your logs--this is because, by default, Laravel 5 has CSRF protection enabled on all routes. You can remove the CSRF protection Middleware from the $middleware stack in App\Http\Kernel and move it to the $routeMiddleware stack as an optional key, or you can adjust all of your forms--even those in AJAX--to ensure they all use CSRF.


Comments? I'm @stauffermatt on Twitter


Tags: laravel  •  laravel 5


This is part of a series of posts on New Features in Laravel 5.0:

  1. Sep 10, 2014 | laravel, 5.0, laravel 5
  2. Sep 10, 2014 | laravel, 5.0, laravel 5
  3. Sep 12, 2014 | laravel, laravel 5, 5.0
  4. Sep 20, 2014 | laravel, 5.0, laravel 5
  5. Sep 28, 2014 | laravel, laravel 5, 5.0
  6. Sep 30, 2014 | laravel, 5.0, laravel 5
  7. Oct 9, 2014 | laravel, 5.0, laravel 5
  8. Oct 10, 2014 | laravel, 5.0, laravel 5
  9. Oct 10, 2014 | laravel, 5.0, laravel 5
  10. Nov 20, 2014 | laravel, 5.0, laravel 5
  11. Jan 2, 2015 | laravel, 5.0, commands, laravel 5
  12. Jan 16, 2015 | laravel, laravel 5
  13. Jan 19, 2015 | laravel 5, laravel
  14. Jan 21, 2015 | laravel, events, 5.0, laravel 5
  15. Jan 26, 2015 | laravel, laravel 5
  16. Feb 1, 2015 | laravel, laravel 5
  17. Feb 14, 2015 | laravel 5, laravel, eloquent

Subscribe

For quick links to fresh content, and for more thoughts that don't make it to the blog.