Working with a Laravel 4 + AngularJS application
October 27, 2014 workflow laravel
So you decided that Laravel
is a great choice for a backend and that Angular
is going to fit perfectly as the framework of choice for the frontend. Yeah! That is correct and I am starting to like you.
How to start? So many question, so little time (someone said :P)
Before we start, let me explain a little bit the different options we have for combining this two.
Isn’t there a perfect solution which work in any case? No. It all depend on the project itself and the use case. I am going to show here my opinions about the subject.
Let’s start with the option of letting Laravel
serve Angular
for you.
PROS:
- It is the simplest way of combining this two.
- In development, it is easier to communicate the
Angular
withLaravel
. There is no need of CORS or proxies. - Makes deployment a breeze. Just tell
forge
to compile the angular before doing any deployment and you are good to go. - It is perfect for newbies and you can always move to other workflows later without problem if needed.
- Perfect for small projects and pet projects. I even see big projects using this workflow tho.
- Since everything is inside the same domain, you just need one SSL certificate, ideal for pet projects.
- We can easily bootstrap data from
Laravel
at startup with some minimum changes to this workflow.
CONS:
- It is a little bit (just a little) coupled. Letting
Laravel
serve ourAngular
couples the solution a little bit. It could be problematic for some kind of projects. - If you want to change the frontend, you need to delete everything you created for
Angular
but luckily for us, you just need to delete one folder. - If you want to change the backend, you need to move that folder somewhere else.
Angular
in one domain (example.com/
) and Laravel
in another domain (api.example.com/
):
PROS:
- Totally decoupled, they know nothing of each other, so we have complete freedom.
- You can swap
Laravel
orAngular
in a breeze. - Works for big projects without problem.
- Maybe little bit more daunting if you’re just starting to learn
frontend
stuff.
CONS:
- You need two SSL certificates, I know, they are cheap but if you just have a pet project, you don’t want to pay double (not a problem if it is an enterprise project).
- Communication between both of them, even when still easy, needs more work (which is not handy for newbies).
There is a middleground between both, having them separate but inside the same domain (Angular
at example.com
and Laravel
at example.com/api
)
PROS:
- Easy to deploy
- You just need one SSL certificate.
CONS:
- If you’re using
Homestead
you will need to craft your ownnginx
config in the new applications. - Makes API versioning maybe a little bit more complicated.
In my opinion, the first way works really good, it is done in a lot of different backends (just google about XXX + angular) and you will see a lot of people doing it. There is nothing wrong with it.
If you feel that your project is going to be big and you prefer a complete and total separation there, that is also awesome, you can do it.
So the TL;DR here is that there are a couple of ways (and more I didn’t mention here) and luckily we have the freedom of picking the one who fits our project. But before doing that, we need to learn how to use them :)
So for this first article, we are going to use the first way
So, right from the beginning:
$ laravel new laravelangulardemo
$ cd laravelangulardemo
Blimey! We arrived. Thank you reading this article.
No, wait, we forgot the Angular part.
As I said before, we are going to follow the first approach here, so I prepared a little workflow here that we can use to accomplish this easily.
Assuming we are on the directory of our Laravel
application we do:
$ git clone https://github.com/Foxandxss/angular-laravel4-workflow angular
$ cd angular
$ npm install
To use the Angular
workflow we just need to clone it on the application root and install its dependencies. We can choose any name for the workflow, I chose angular
because it is a pretty obvious name. You can clone it on the /app
directory if you wish (check the workflow repository for more information).
Let’s create a git repo for our project. From the root directory we:
$ rm -rf angular/.git
$ git init
We delete the repository that comes with the workflow and we create a new one for the whole application, then we modify our .gitignore
to support our Angular
workflow:
/bootstrap/compiled.php
/vendor
composer.phar
composer.lock
.env.*.php
.env.php
.DS_Store
Thumbs.db
# Angular ignores
angular/node_modules
public/images
public/css
public/js
public/angular.html
Ignoring those ignores for now, we can now commit:
$ git add .
$ git commit -m "Initial commit"
Now assuming we have a remote repository (I have mine for this project here):
$ git push
Alright, everything is set in place, let’s do a Hello World
first to see everthing working.
First, we just need to start gulp
which is going to do all the heavy lifting for us:
$ cd angular
$ gulp
And then we modify our index.html
file to put that Hello World
.
File: angular/app/index.html
<!doctype html>
<html ng-app="app" lang="en">
<head>
<meta charset="UTF-8">
<title>Angular App</title>
<link rel="stylesheet" href="<%= css %>">
</head>
<body>
Hello, World, this is angular and to prove it, there is a computation: 5 + 3 == {{ 5 + 3 }}
<script type="text/javascript" src="<%= js %>"></script>
</body>
</html>
Here we have the message with a little angular to prove it is working. Notice that the ng-app
is already set for us.
Also, don’t be scared with those <%= ... %>
placeholders, that is for the cache-busting
and we don’t need to worry about that.
If you take a look to our /public
folder (the Laravel
one) we can see:
There is our compiled angular
application! (In concrete, the css
and js
folders along the angular.html
)
Why angular.html
instead of index.html
? Well, the nginx
could load that for us and in a first sight there is no issue, but I am not an experienced Laravel
developer so it could have some side effect and I don’t want that. Changing the name to angular.html
will prevent that.
Great, but if we run our application…
Uh, what happened with our Hello World
?
Well, nothing, the question is… Who is routing it? Laravel
knows nothing about that file, we need to create a route for it:
File: app/routes.php
App::missing(function($exception)
{
return File::get(public_path() . '/angular.html');
});
We delete all its content and we put that.
What can you tell me about this complex route? This is a catch-all
route. What’s that? A catch-all
route is a route we put at the END that will catch any request (that is why we put it last). You can read it like… If after all the routes I have on the file, you end here, please get the /public/angular.html
file and give it back. Just what we needed. We can define all our /api
routes before that and when our request doesn’t match any REST endpoint
it will serve the angular
. Later you will see another advantage.
If we run the app now, we see:
Fantastic! It simply works!
We can create a Laravel controller
to serve that /public/angular.html
but we don’t really need to. Uh, talking about controllers… There is one we don’t need:
$ git rm app/controllers/HomeController.php
$ git rm app/views/hello.php
We didn’t need that view as well.
Let’s commit the changes:
$ git add .
$ git commit -m "Angular is now working"
$ git push
Now we are going to implement a simple endpoint to serve a list of our favorite TV shows:
File: app/controllers/ShowController.php
<?php
class ShowController extends \BaseController
{
/**
* Display a listing of the shows.
*
* @return Response::json
*/
public function index()
{
$shows = array('Doctor Who', 'Stargate SG1', 'Once upon a time',
'The Blacklist', 'Prison Break', 'White Collar');
return Response::json($shows);
}
}
File: app/routes.php
Route::get('api/shows', 'ShowController@index');
App::missing(function($exception)
{
return File::get(public_path() . '/angular.html');
});
Excuse my Laravel
fu, I am new at it.
So, a controller which returns an array of TV shows and a route to serve them (notice how I put it before the catch-all
).
We know need to consume it from Angular
.
For the sake of completion, we are going to pull angular-route.js
from here and we just need to download it to angular/vendor/js
. There is no bower here, I don’t like it.
Well, we don’t need to tell anyone about that new file, just put is a dependency:
File: angular/app/js/app.js
angular.module('app', ['ngRoute']);
Then we configure ngRoute
a little bit:
File: angular/app/js/config.routes.js
angular.module('app').config(function($routeProvider) {
$routeProvider.otherwise('/shows');
});
We redirect to ‘/shows’ if the route doesn’t match anything registered.
Now we create a new feature for the show listing:
File: angular/app/js/features/shows/config.routes.js
angular.module('app').config(function($routeProvider) {
$routeProvider.when('/shows', {
templateUrl: 'features/shows/shows.tpl.html',
controller: 'Shows'
});
});
File: angular/app/js/features/shows/shows.js
angular.module('app').controller('Shows', function($scope, $http) {
$http.get('/api/shows').then(function(result) {
$scope.shows = result.data;
});
});
File: angular/app/js/features/shows/shows.tpl.html
<h1>My favorite TV Shows ever</h1>
<ul>
<li ng-repeat="show in shows">
{{show}}
</li>
</ul>
We create a new route for the /shows
pointing to our template and controller. The controller will forget a little bit about angular conventions and will make a $http
request to our Laravel
endpoint and will save the result on $scope.shows
. Lastly, our template will show them on a list.
The last piece is our index.html
which needs a ng-view
:
File: angular/app/index.html
<body>
<div ng-view></div>
<script type="text/javascript" src="<%= js %>"></script>
</body>
If we execute this:
Here it is! Working like charm.
Notice the /#/
at the url, that is something we expect on an Angular
app. What if we want to use html5mode
to remove it?
File: angular/app/js/config.routes.js
angular.module('app').config(function($routeProvider, $locationProvider) {
$routeProvider.otherwise('/shows');
$locationProvider.html5Mode(true).hashPrefix('!');
});
With that new change, we are using html5mode
. That also needs a little change on the index.html
as well:
File: angular/app/index.html
<head>
<meta charset="UTF-8">
<title>Angular App</title>
<link rel="stylesheet" href="<%= css %>">
<base href="/" />
</head>
Just that base
tag.
Oh, now it doesn’t have that /#/
.
If you’re curious about how this work, it is something like:
- You request
/
- It goes to Laravel and it starts looking the
routes.php
file. - Is
/api/shows
matching/
? No. - Ok, we ran out of matches, so here is the
catch-all
route. catch-all
route will render the angular and will give to it the/
requested route.- Angular will give that
/
requested route to its router. - No match, so it runs the
otherwise
which redirects to/shows
.
Let’s commit our changes:
$ git add .
$ git commit -m "TV Shows endpoint + angular consumption"
$ git push
Our application is done! What about preparing our Angular
for production?
Easy:
$ cd angular
$ gulp clean && gulp production
You will notice that our angular.html
is different now:
File: public/angular.html
<!doctype html>
<html ng-app="app" lang="en">
<head>
<meta charset="UTF-8">
<title>Angular App</title>
<link rel="stylesheet" href="app-d41d8cd9.css">
<base href="/" />
</head>
<body>
<div ng-view></div>
<script type="text/javascript" src="app-b9f3917d.js"></script>
</body>
</html>
Our assets have a hash to do cache-busting.
You can simply tell forge
to run that before any deployment and you’re good to go.
If you feel like this way is not for you, worry not, in a future article, we are going to play with the third option of having both separated but in the same domain.