PHP
Article
By Jad Joubran

Flexible and Easily Maintainable Laravel + Angular Material Apps

By Jad Joubran

In this article, we’re going to set up a Laravel API with Angular Material for the front end. We’re also going to follow best practices that will help us scale with the number of developers working on the project and the complexity behind it. Most tutorials cover this topic from another perspective – they completely forget about scaling. While this tutorial is not targeted at small todo apps, it is extremely helpful if you’re planning to work with other developers on a big project.

Laravel angular material

Here’s a demo built with Laravel and Angular Material.

Setting up Laravel

Creating a project

We’re going to start by pulling in the latest Laravel – 5.1 at the time of writing.

composer create-project laravel/laravel myapp --prefer-dist

Configuring the environment

All the subsequent commands will be ran inside the myapp directory. Throughout the remainder of this tutorial, we’ll assume you’re running a Linux environment if you’re following along. If you aren’t you’re encouraged to install Homestead Improved as a virtual Linux environment in which to follow along.

cd myapp

Next, we’re going to update our .env file with the database connection credentials:

DB_HOST=localhost
DB_DATABASE=your-db-name
DB_USERNAME=your-username
DB_PASSWORD=your-password

Once your app has been configured, you should see the Laravel 5 greeting page.

Laravel 5

Debugbar

Laravel debug bar is one of the most useful Laravel packages.

Debugbar makes it easy to debug any of the following:

  • exceptions
  • views
  • messages
  • queries
  • mails
  • auth
  • routes
  • ajax
  • and more

Laravel debugbar

So let’s go ahead and install it using composer

composer require barryvdh/laravel-debugbar

Next, we need to open config/app.php and:

  • add Barryvdh\Debugbar\ServiceProvider::class, to the list of providers
  • add 'Debugbar' => Barryvdh\Debugbar\Facade::class, to the list of aliases

Refresh the page and you should see debugbar at the bottom of the page!

Debugbar runs automatically if you have the APP_DEBUG flag enabled in your .env file.

Build automation

Laravel’s Elixir is a layer on top of gulp that makes it easier to write gulp tasks.

Setup

We’ll start by installing gulp globally. Note that you need nodeJS installed for this section.

npm install -g gulp

and then we need to grab a few packages that will make our lives easier, so replace your package.json with the following:

{
  "dependencies": {
    "gulp-concat": "^2.6.0",
    "gulp-concat-sourcemap": "^1.3.1",
    "gulp-filter": "^1.0.2",
    "gulp-if": "^1.2.5",
    "gulp-jshint": "^1.9.0",
    "gulp-minify-css": "^0.3.11",
    "gulp-ng-annotate": "^1",
    "gulp-notify": "^2.0.0",
    "gulp-sourcemaps": "^1",
    "gulp-uglify": "^1",
    "jshint-stylish": "^2",
    "laravel-elixir": "^3.0.0",
    "laravel-elixir-livereload": "^1.1.1",
    "main-bower-files": "^2.1.0"
  },
  "devDependencies": {
    "gulp": "^3.9.0"
  }
}

Then install these packages.

npm install

Managing front-end dependencies

I like to use Bower because of its flat dependency tree, but that’s really up to your preference.
You can use npm, requirejs or just the plain old browse-to-url-and-download-and-then-manually-check-for-updates.

Let’s install bower globally.

npm install -g bower

Then add this line to .gitignore:

/bower_components

and run bower init to create a new bower.json file which will be committed in the repository.

Folder by Feature

Then, we want to choose a location for Angular within our Laravel folder.

Most people prefer to add it to resources/js but since I prefer to have a folder by feature architecture, we’re going to create an angular folder at the root level.

I chose this setup because we want it to be able to scale with the number of developers and with the business complexity behind it.

A few months from now, we’re going to have a folder for every feature. Here’s an example of the folders that we are going to have inside angular/app:

  • header
    • header.html
    • header.js
    • header.less
  • login
    • login.html
    • login.js
    • login.less
    • service.js
  • landing
  • users
  • change_password
  • reset_password
  • list_items
  • add_item
  • item_details

It’s much easier to fix bugs in the list_items page. We just have to open the list_items folder where we find all the related files for this feature:

  • templates
  • Less files
  • Angular controller(s)
  • Angular service(s).

Compare this to having to open a folder for each of these files when debugging a single feature.

So let’s go ahead and create the following folders:

  • angular
  • angular/app
  • angular/services
  • angular/filters
  • angular/directives
  • angular/config

Elixir configuration

Now we need to configure elixir to compile a js/vendor.js and a css/vendor.css file from the bower_components folder.

Then we need to run our angular folder through the elixir angular task, which does the following:

  • concatenates our app files into public/js/app.js
  • validates with jshint (if the .jshintrc file is available). If the file is available, it won’t recompile our source code if your code does not pass validation.
  • automatic dependency annotation using ng-annotate

We need to compile our less files. If you’re using Sass, you probably know how to make it work for Sass.

We also need to copy our views from within the angular directory to the public directory.

Finally, we are going to set up livereload. Soft reloads are used for CSS, meaning that a newer version of your css will be injected without reloading the page.

Let’s open gulpfile.js and replace the sample Elixir function call with the following:

var elixir = require('laravel-elixir');
require('./tasks/angular.task.js');
require('./tasks/bower.task.js');
require('laravel-elixir-livereload');

elixir(function(mix){
    mix
        .bower()
        .angular('./angular/')
        .less('./angular/**/*.less', 'public/css')
        .copy('./angular/app/**/*.html', 'public/views/app/')
        .copy('./angular/directives/**/*.html', 'public/views/directives/')
        .copy('./angular/dialogs/**/*.html', 'public/views/dialogs/')
        .livereload([
            'public/js/vendor.js',
            'public/js/app.js',
            'public/css/vendor.css',
            'public/css/app.css',
            'public/views/**/*.html'
        ], {liveCSS: true});
});

If you noticed, I’m loading a couple of custom tasks, so you just need to create the tasks folder at the root directory of the project and create these 2 files:

tasks/angular.task.js

/*Elixir Task
*copyrights to https://github.com/HRcc/laravel-elixir-angular
*/
var gulp = require('gulp');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');
var jshint = require('gulp-jshint');
var stylish = require('jshint-stylish');
var uglify = require('gulp-uglify');
var ngAnnotate = require('gulp-ng-annotate');
var notify = require('gulp-notify');
var gulpif = require('gulp-if');

var Elixir = require('laravel-elixir');

var Task = Elixir.Task;

Elixir.extend('angular', function(src, output, outputFilename) {

    var baseDir = src || Elixir.config.assetsPath + '/angular/';

    new Task('angular in ' + baseDir, function() {
        // Main file has to be included first.
        return gulp.src([baseDir + "main.js", baseDir + "**/*.js"])
            .pipe(jshint())
            .pipe(jshint.reporter(stylish))
            //.pipe(jshint.reporter('fail')).on('error', onError) //enable this if you want to force jshint to validate
            .pipe(gulpif(! config.production, sourcemaps.init()))
            .pipe(concat(outputFilename || 'app.js'))
            .pipe(ngAnnotate())
            .pipe(gulpif(config.production, uglify()))
            .pipe(gulpif(! config.production, sourcemaps.write()))
            .pipe(gulp.dest(output || config.js.outputFolder))
            .pipe(notify({
                title: 'Laravel Elixir',
                subtitle: 'Angular Compiled!',
                icon: __dirname + '/../node_modules/laravel-elixir/icons/laravel.png',
                message: ' '
            }));
    }).watch(baseDir + '/**/*.js');

});

tasks/bower.task.js

/*Elixir Task for bower
* Upgraded from https://github.com/ansata-biz/laravel-elixir-bower
*/
var gulp = require('gulp');
var mainBowerFiles = require('main-bower-files');
var filter = require('gulp-filter');
var notify = require('gulp-notify');
var minify = require('gulp-minify-css');
var uglify = require('gulp-uglify');
var concat_sm = require('gulp-concat-sourcemap');
var concat = require('gulp-concat');
var gulpIf = require('gulp-if');

var Elixir = require('laravel-elixir');

var Task = Elixir.Task;

Elixir.extend('bower', function(jsOutputFile, jsOutputFolder, cssOutputFile, cssOutputFolder) {

    var cssFile = cssOutputFile || 'vendor.css';
    var jsFile = jsOutputFile || 'vendor.js';

    if (!Elixir.config.production){
        concat = concat_sm;
    }

    var onError = function (err) {
        notify.onError({
            title: "Laravel Elixir",
            subtitle: "Bower Files Compilation Failed!",
            message: "Error: <%= error.message %>",
            icon: __dirname + '/../node_modules/laravel-elixir/icons/fail.png'
        })(err);
        this.emit('end');
    };

    new Task('bower-js', function() {
        return gulp.src(mainBowerFiles())
            .on('error', onError)
            .pipe(filter('**/*.js'))
            .pipe(concat(jsFile, {sourcesContent: true}))
            .pipe(gulpIf(Elixir.config.production, uglify()))
            .pipe(gulp.dest(jsOutputFolder || Elixir.config.js.outputFolder))
            .pipe(notify({
                title: 'Laravel Elixir',
                subtitle: 'Javascript Bower Files Imported!',
                icon: __dirname + '/../node_modules/laravel-elixir/icons/laravel.png',
                message: ' '
            }));
    }).watch('bower.json');


    new Task('bower-css', function(){
        return gulp.src(mainBowerFiles())
            .on('error', onError)
            .pipe(filter('**/*.css'))
            .pipe(concat(cssFile))
            .pipe(gulpIf(config.production, minify()))
            .pipe(gulp.dest(cssOutputFolder || config.css.outputFolder))
            .pipe(notify({
                title: 'Laravel Elixir',
                subtitle: 'CSS Bower Files Imported!',
                icon: __dirname + '/../node_modules/laravel-elixir/icons/laravel.png',
                message: ' '
            }));
    }).watch('bower.json');

});

These 2 gulp tasks might look a bit complex, but you don’t have to worry about that. You’re here to speed up your development process rather than wasting time setting up build tools.

The reason why I’m not using some of the available node modules that are available online, is because, at the time of writing, they are slow and often limited in terms of functionality. Enjoy!

Setting up AngularJS

Installation

Let’s get started by downloading the latest version of Angular 1

bower install angular#1 --save

Don’t forget the --save flag because we want this to be saved in bower.json

We can now run gulp && gulp watch.

Configuring Main Module

Let’s start by configuring angular/main.js

(function(){
"use strict";

var app = angular.module('app',
        [
        'app.controllers',
        'app.filters',
        'app.services',
        'app.directives',
        'app.routes',
        'app.config'
        ]);

    angular.module('app.routes', []);
    angular.module('app.controllers', []);
    angular.module('app.filters', []);
    angular.module('app.services', []);
    angular.module('app.directives', []);
    angular.module('app.config', []);
    })();

Bridging Laravel & Angular

Serving the App

We need to create a new controller using artisan.

php artisan make:controller AngularController --plain
AngularController.php


public function serveApp(){
    return view('index');
}

and then replace the existing Route::get('/', ...) in routes.php with the following:

Route::get('/', 'AngularController@serveApp');

This seems useless at first, but I like to keep the logic outside the Routes file, so I prefer not to have closures there. Eventually, we’re going to use this controller for other methods, like the unsupported browser page.

Then, we need to create the view resources/views/index.blade.php and add this HTML:

<html ng-app="app">
<head>
    <link rel="stylesheet" href="/css/vendor.css">
    <link rel="stylesheet" href="/css/app.css">
</head>
<body>

    <md-button class="md-raised md-primary">Welcome to Angular Material</md-button>

    <script src="/js/vendor.js"></script>
    <script src="/js/app.js"></script>
</body>
</html>

Unsupported Browser

Angular Material is targeted at evergreen browsers, so we need to add a page for unsupported ones ( IE <= 10 ).

We start by adding the following conditional comment in the <head> of our index.blade.php view:

<!--[if lte IE 10]>
    <script type="text/javascript">document.location.href ='/unsupported-browser'</script>
    <![endif]-->

This will redirect the user with an unsupported browser to /unsupported-browser, a route which we should handle in routes.php:

Route::get('/unsupported-browser', 'AngularController@unsupported');

Then, inside AngularController we create the unsupported method:

public function unsupported(){
    return view('unsupported');
}

Finally, we create the unsupported.blade.php view and output a message telling the user that they needs to upgrade to a modern browser.

Pulling in Angular Material

Angular Material is an implementation of Material Design in Angular.js. It provides a set of reusable, well-tested, and accessible UI components based on the Material Design system.

Installation

First we pull Angular Material using Bower:

bower install angular-material --save

Then, we add the ngMaterial module to the app.controllers and app.config modules:

angular.module('app.controllers', ['ngMaterial']);
angular.module('app.config', ['ngMaterial']);

Finally, we re-run gulp watch.

Custom theming

You probably love the feel that Angular Material gives to your app, but you’re worried that it’ll look exactly like Angular Material.

We’ll want to apply some branding guidelines to the theme, which is why we need to create a new config provider for Angular Material which allows us to specify the 3 main colors for our theme:

/angular/config/theme.js:


(function(){
    "use strict";

    angular.module('app.config').config( function($mdThemingProvider) {
        /* For more info, visit https://material.angularjs.org/#/Theming/01_introduction */
        $mdThemingProvider.theme('default')
        .primaryPalette('teal')
        .accentPalette('cyan')
        .warnPalette('red');
    });

})();

Setting up ui-router

ui-router is the de-facto solution to flexible routing with nested views.

Installation

Let’s start by pulling that in using bower:

bower install  ui-router --save

Then, add the ui.router module to the app.routes and app.controllers modules:

angular.module('app.routes', ['ui.router']);
angular.module('app.controllers', ['ngMaterial', 'ui.router']);

Then, we re-run gulp watch.

Configuring routes

Now we need to set up our routes file. Let’s go ahead and create angular/routes.js

(function(){
    "use strict";

    angular.module('app.routes').config( function($stateProvider, $urlRouterProvider ) {

        var getView = function( viewName ){
            return '/views/app/' + viewName + '/' + viewName + '.html';
        };

        $urlRouterProvider.otherwise('/');

        $stateProvider
        .state('landing', {
            url: '/',
            views: {
                main: {
                    templateUrl: getView('landing')
                }
            }
        }).state('login', {
            url: '/login',
            views: {
                main: {
                    templateUrl: getView('login')
                },
                footer: {
                    templateUrl: getView('footer')
                }
            }
        });

    } );
})();

We created 2 routes, one for the Landing page and the other for the Login page. Notice how we have multiple named views. We need to add that to our main view, inside the <body>:

<div ui-view="main"></div>
<div ui-view="footer"></div>

We then create the following folders and files inside angular/app:

  • landing/landing.html with the output Landing view
  • login/login.html with the output Login view
  • footer/footer.html with the output Footer view

Now, whenever you need to add a new page, you just have to add a new .state().

--ADVERTISEMENT--

Setting up Restangular

Restangular is an AngularJS service that simplifies common ajax calls to a RESTful API.

Installation

Restangular is a perfect fit with our Laravel API.

Let’s grab the latest version of Restangular by running the following:

bower install restangular --save

Then, add the restangular module to the app.controllers module:

angular/main.js:

angular.module('app.controllers', ['ngMaterial', 'ui.router', 'restangular']);

Then, re-run gulp watch.

Let’s set up a sample API endpoint.

php artisan make:controller DataController --plain
DataController.php:

public function index(){
    return ['data', 'here'];
}
app\Http\routes.php:

Route::get('/data', 'DataController@index');

Sample Usage:

Restangular.all('data').doGET().then(function(response){
    window.console.log(response);
});

Toast

A toast provides simple feedback about an operation in a small popup.

Toast

Since we’re going to use toasts a lot in our application, we’re going to create a toast service angular/services/toast.js

(function(){
    "use strict";

    angular.module("app.services").factory('ToastService', function( $mdToast ){

        var delay = 6000,
        position = 'top right',
        action = 'OK';

        return {
            show: function(content) {
                return $mdToast.show(
                    $mdToast.simple()
                    .content(content)
                    .position(position)
                    .action(action)
                    .hideDelay(delay)
                    );
            }
        };
    });
})();

And now here’s how we can use it in our app:

(function(){
    "use strict";

    angular.module('app.controllers').controller('TestCtrl', function( ToastService ){
                    ToastService.show('User added successfully');
            };

        });

})();

Dialogs

Dialogs are one of the most useful features available in Angular Material. They’re very similar to Modals in Twitter Bootstrap.

Material Dialog

Dialogs are a key component in Single Page Apps, that’s why we’re going to write a powerful dialog service /angular/services/dialog.js

(function(){
    "use strict";

    angular.module("app.services").factory('DialogService', function( $mdDialog ){

        return {
            fromTemplate: function( template, $scope ) {

                var options = {
                    templateUrl: '/views/dialogs/' + template + '/' + template + '.html'
                };

                if ( $scope ){
                    options.scope = $scope.$new();
                }

                return $mdDialog.show(options);
            },

            hide: function(){
                return $mdDialog.hide();
            },

            alert: function(title, content){
                $mdDialog.show(
                    $mdDialog.alert()
                    .title(title)
                    .content(content)
                    .ok('Ok')
                    );
            }
        };
    });
})();

We created 3 methods inside this service:

  • alert(title, content) allows us to display a dialog with a title and a message. Useful for error and success messages
  • hide() hides the dialog
  • fromTemplate(template, $scope) creates a dialog from a template stored in /angular/dialogs/. Useful for Login, Reigster, etc. dialogs. You can create your own component inside the /angular/dialogs/ directory using the same folder by feature approach. You can also pass $scope to the dialog, which will give you access to the $parent scope from within the dialog’s controller.

We just need to fix the Elixir configuration to watch and copy the /angular/dialogs folder:

 .copy('angular/dialogs/**/*.html', 'public/views/dialogs/');

And now here’s how we can use it in our app:

(function (){
    "use strict";

    angular.module('app.controllers').controller('DialogTestCtrl', function ($scope, DialogService){


            $scope.addUser = function(){
                return DialogService.fromTemplate('add_user', $scope);
            };

            $scope.success = function(){
                return DialogService.alert('Success', 'User created successfully!');
            };


        });

})();

Deployment

Here’s a plain bash script that we’re going to use for deployment. You can save it as deploy.sh.

You’d just need to prepend it with an ssh command to your server ssh@your-domain.

php artisan route:clear
php artisan config:clear
git pull
php artisan migrate
composer install
php artisan route:cache
php artisan config:cache
php artisan optimize

The first two commands clear the route and configuration cache, which will then be generated again after pulling the new code. This will greatly improve the performance of your app when running in production.

Don’t forget that any configuration/routing change you make will not take effect until you clear the cache again.

Code Quality

Enforcing code quality helps the maintenance of big projects. You don’t want to end up with a terrible code base a few months from now. This is completely optional, but I’d recommend you set up some automated code quality tools.

We’re going to start by installing the necessary node modules:

npm install -g jshint jscs

EditorConfig

EditorConfig helps us maintain a consistent coding style between different editors and IDEs.
This is especially useful when you have many developers/contributors working on the same project.

You don’t want someone to push code with spaces instead of tabs, or vice versa, CRLF as line endings instead of LF, or vice versa.

Let’s create the.editorconfig file at the root level. Feel free to switch between CRLF and LF, tabs and spaces, etc.. it all depends on your coding style.

root = true

[*]
insert_final_newline = false
charset = utf-8
end_of_line = lf

[*.{js,html}]
indent_size = 4
indent_style = tab
trim_trailing_whitespace = true

[{package.json,bower.json,.jscs.json}]
indent_style = space
indent_size = 2

[*.sh]
end_of_line = lf

Depending on your code editor, you might need to download a plugin for editorConfig.

This is also convenient when you are working on more than 1 project using the same text editor and each project has different coding style guidelines.

JSHINT

Jshint is a Javascript code quality tool that helps to detect errors and potential problems in Javascript code.

It also enforces coding conventions on your team.

We need to create a .jshintrc file at the root level of the project.
You can browse the available options for jshint here.
Note that when you add a .jshintrc file, the angular task we have in our gulpfile will not recompile our code if it doesn’t validate according to jshint.

Here’s a recommended jshintrc for our scenario. Feel free to modify it according to your coding style.

{
  "browser": true,
  "bitwise": true,
  "immed": true,
  "newcap": false,
  "noarg": true,
  "noempty": true,
  "nonew": true,
  "maxlen": 140,
  "boss": true,
  "eqnull": true,
  "eqeqeq": true,
  "expr": true,
  "strict": true,
  "loopfunc": true,
  "sub": true,
  "undef": true,
  "globals": {
    "angular": false,
    "describe": false,
    "it": false,
    "expect": false,
    "beforeEach": false,
    "afterEach": false,
    "module": false,
    "inject": false
  }
}

JSCS

JSCS is a code style linter for programmatically enforcing your style guide.

We’re going to create a .jscs.json file at the root level.
Feel free to modify it depending on your style.

{
  "requireCurlyBraces": [
    "if",
    "else",
    "for",
    "while",
    "do"
  ],
  "requireSpaceAfterKeywords": [
    "if",
    "for",
    "while",
    "do",
    "switch",
    "return"
  ],
  "disallowSpacesInFunctionExpression": {
    "beforeOpeningRoundBrace": true
  },
  "disallowTrailingWhitespace": true,
  "disallowMixedSpacesAndTabs": true,
  "requireMultipleVarDecl": true,
  "requireSpacesInsideObjectBrackets": "all",
  "requireSpaceBeforeBinaryOperators": [
    "+",
    "-",
    "/",
    "*",
    "=",
    "==",
    "===",
    "!=",
    "!=="
  ],
  "disallowSpaceAfterPrefixUnaryOperators": [
    "++",
    "--",
    "+",
    "-"
  ],
  "disallowSpaceBeforePostfixUnaryOperators": [
    "++",
    "--"
  ],
  "disallowKeywords": [
    "with"
  ],
  "disallowKeywordsOnNewLine": [
    "else"
  ],
  "excludeFiles": ["node_modules/**"]
}

PHPCS

Just like jshint, we need to be able to enforce code cleanliness and consistency for our PHP files.

Let’s start by installing phpcs globally:

composer global require "squizlabs/php_codesniffer=*"

Now we can use the following command to check the app folder:

phpcs --standard=psr2 app/

The cool thing here is that we can use PSR2 as a coding standard which is used by Laravel, so we don’t have to set up a custom configuration file.

Git hooks

Jshint and jscs are great tools, but they need to be automatically enforced or else we’ll forget to lint our code.

You can optionally install the corresponding linter plugins for your text editor or IDE, but one of the recommended ways of doing it would be to run these linters as part of your git commit process.

Create .git/hooks/pre-commit if it does not exist and add the following:

#!/bin/sh
jscs angular/**/*.js
jshint angular/**/*.js
phpcs --standard=psr2 app/
exec 1>&2

Then run chmod +x .git/hooks/pre-commit.

Don’t forget to add this for each developer who joins your team, and every time someone pushes, they’ll be automatically warned of possible issues.

Conclusion

This article helped us set up a scalable Laravel and Angular Material app. You can also grab the source code of this tutorial on github: laravel5-angular-material-starter. If you’ve been using Angular with Bootstrap, I’d recommend you give Angular Material a try. Do you have any questions? I’d love to know what you think. Just let me know in the comments!

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.