Upgrading from Laravel 4 to Laravel 5
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:
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 touse Auth
,use Session
, etc. at the top of the controller—or just prepend\
to each inline (e.g. convertSession::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!
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:
-
Sep 10, 2014 | laravel, 5.0, laravel 5
-
Sep 10, 2014 | laravel, 5.0, laravel 5
-
Sep 12, 2014 | laravel, laravel 5, 5.0
-
Sep 20, 2014 | laravel, 5.0, laravel 5
-
Sep 28, 2014 | laravel, laravel 5, 5.0
-
Sep 30, 2014 | laravel, 5.0, laravel 5
-
Oct 9, 2014 | laravel, 5.0, laravel 5
-
Oct 10, 2014 | laravel, 5.0, laravel 5
-
Oct 10, 2014 | laravel, 5.0, laravel 5
-
Oct 16, 2014 | laravel, 5.0, laravel 5
-
Nov 20, 2014 | laravel, 5.0, laravel 5
-
Jan 2, 2015 | laravel, 5.0, commands, laravel 5
-
Jan 16, 2015 | laravel, laravel 5
-
Jan 19, 2015 | laravel 5, laravel
-
Jan 21, 2015 | laravel, events, 5.0, laravel 5
-
Jan 26, 2015 | laravel, laravel 5
-
Feb 1, 2015 | laravel, laravel 5
-
Feb 14, 2015 | laravel 5, laravel, eloquent