Building a Phonegap app with Laravel and Angular - Part 3

Published by at 16th October 2016 5:10 pm

Apologies for how long it's taken for this post to appear. I've got a lot on my plate at present as I recently started a new job, so I haven't been able to devote as much time to this series as I'd like.

In this instalment we'll begin extending our app beyond the basic authentication we've already implemented. We'll start by adding the means to sign up, before adding the list of pets.

Adding a signup method to our backend

We'll create a controller for our users in the Laravel backend. First we'll create our tests:

$ php artisan make:test UserControllerTest

We'll create three tests. The first will check to see that an invalid request raises the correct status code (422). The second will check that a valid request returns the correct status code (201) and creates the user. The third will check that trying to create a duplicate user raises an error. Here they are - they should be saved in the new tests/UserControllerTest.php file:

1<?php
2
3use Illuminate\Foundation\Testing\DatabaseMigrations;
4
5class UserControllerTest extends TestCase
6{
7 /**
8 * Test creating a user - invalid
9 *
10 * @return void
11 */
12 public function testPostingInvalidUser()
13 {
14 // Create a request
15 $data = array(
16 'name' => 'Bob Smith',
17 'email' => 'bob@example.com'
18 );
19 $this->json('POST', '/api/users', $data);
20 $this->assertResponseStatus(422);
21 }
22
23 /**
24 * Test creating a user
25 *
26 * @return void
27 */
28 public function testPostingUser()
29 {
30 // Create a request
31 $data = array(
32 'name' => 'Bob Smith',
33 'email' => 'bob@example.com',
34 'password' => 'password',
35 'password_confirmation' => 'password'
36 );
37 $this->json('POST', '/api/users', $data);
38 $this->assertResponseStatus(201);
39 $this->seeInDatabase('users', ['email' => 'bob@example.com']);
40
41 // Check user exists
42 $saved = User::first();
43 $this->assertEquals($saved->email, 'bob@example.com');
44 $this->assertEquals($saved->name, 'Bob Smith');
45 }
46
47 /**
48 * Test creating a duplicate user
49 *
50 * @return void
51 */
52 public function testPostingDuplicateUser()
53 {
54 // Create user
55 $user = factory(AnimalFriend\User::class)->create([
56 'name' => 'Bob Smith',
57 'email' => 'bob@example.com',
58 'password' => 'password'
59 ]);
60 $this->seeInDatabase('users', ['email' => 'bob@example.com']);
61
62 // Create a request
63 $data = array(
64 'name' => 'Bob Smith',
65 'email' => 'bob@example.com',
66 'password' => 'password',
67 'password_confirmation' => 'password'
68 );
69 $this->json('POST', '/api/users', $data);
70 $this->assertResponseStatus(422);
71 }
72}

Note the use of $this->json() to make the request. This method is ideal for testing a REST API.

Running our tests should confirm that they fail:

1$ vendor/bin/phpunit
2PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
3
4........FFF. 12 / 12 (100%)
5
6Time: 827 ms, Memory: 18.00MB
7
8There were 3 failures:
9
101) UserControllerTest::testPostingInvalidUser
11Expected status code 422, got 404.
12Failed asserting that 404 matches expected 422.
13
14/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
15/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:21
16
172) UserControllerTest::testPostingUser
18Expected status code 201, got 404.
19Failed asserting that 404 matches expected 201.
20
21/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
22/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:39
23
243) UserControllerTest::testPostingDuplicateUser
25Expected status code 422, got 404.
26Failed asserting that 404 matches expected 422.
27
28/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:648
29/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:71
30
31FAILURES!
32Tests: 12, Assertions: 43, Failures: 3.

Next, we create our new controller:

$ php artisan make:controller UserController --resource

Let's populate it:

1<?php
2
3namespace AnimalFriend\Http\Controllers;
4
5use Illuminate\Http\Request;
6
7use AnimalFriend\Http\Requests;
8use AnimalFriend\User;
9use JWTAuth;
10use Hash;
11
12class UserController extends Controller
13{
14 private $user;
15
16 public function __construct(User $user) {
17 $this->user = $user;
18 }
19
20 /**
21 * Display a listing of the resource.
22 *
23 * @return \Illuminate\Http\Response
24 */
25 public function index()
26 {
27 //
28 }
29
30 /**
31 * Show the form for creating a new resource.
32 *
33 * @return \Illuminate\Http\Response
34 */
35 public function create()
36 {
37 //
38 }
39
40 /**
41 * Store a newly created resource in storage.
42 *
43 * @param \Illuminate\Http\Request $request
44 * @return \Illuminate\Http\Response
45 */
46 public function store(Request $request)
47 {
48 // Validate request
49 $valid = $this->validate($request, [
50 'email' => 'required|email|unique:users,email',
51 'name' => 'required|string',
52 'password' => 'required|confirmed',
53 ]);
54
55 // Create user
56 $user = new $this->user;
57 $user->email = $request->input('email');
58 $user->name = $request->input('name');
59 $user->password = Hash::make($request->input('password'));
60 $user->save();
61
62 // Create token
63 $token = JWTAuth::fromUser($user);
64
65 // Send response
66 return response()->json(['token' => $token], 201);
67 }
68
69 /**
70 * Display the specified resource.
71 *
72 * @param int $id
73 * @return \Illuminate\Http\Response
74 */
75 public function show($id)
76 {
77 //
78 }
79
80 /**
81 * Show the form for editing the specified resource.
82 *
83 * @param int $id
84 * @return \Illuminate\Http\Response
85 */
86 public function edit($id)
87 {
88 //
89 }
90
91 /**
92 * Update the specified resource in storage.
93 *
94 * @param \Illuminate\Http\Request $request
95 * @param int $id
96 * @return \Illuminate\Http\Response
97 */
98 public function update(Request $request, $id)
99 {
100 //
101 }
102
103 /**
104 * Remove the specified resource from storage.
105 *
106 * @param int $id
107 * @return \Illuminate\Http\Response
108 */
109 public function destroy($id)
110 {
111 //
112 }
113}

For now we'll leave the other methods blank, but we'll be using them later so we won't get rid of them. At the top, note we load not only the User model, but also the JWTAuth and Hash facades. We use JWTAuth::fromUser() to return a JSON web token for the given user model.

In the store() method we first of all use Laravel's validation support to validate our input. We specify that the user must provide a unique email address, a username, and a password, which must be confirmed. Note that we don't need to specify an action if the request is invalid, as Laravel will do that for us. Also, note that the confirmed rule means that the password field must be accompanied by a matching password_confirmation field.

Next, we create the user. Note that we hash the password before storing it, which is a best practice (storing passwords in plain text is a REALLY bad idea!). Then we create the token for the new user and return it. From then on, the user can use that token to authenticate their requests.

We also need to add this route in routes/api.php:

Route::resource('users', 'UserController');

Let's check the test passes:

1$ vendor/bin/phpunit
2PHPUnit 5.5.4 by Sebastian Bergmann and contributors.
3
4............ 12 / 12 (100%)
5
6Time: 905 ms, Memory: 20.00MB
7
8OK (12 tests, 46 assertions)

Building the registration in the app

With registration in place on the server side, we can move back to the app. We need to create another route for the registration form. Add this to test/routes.spec.js:

1 it('should map register route to register controller', function () {
2 inject(function ($route) {
3 expect($route.routes['/register'].controller).toBe('RegisterCtrl');
4 expect($route.routes['/register'].templateUrl).toEqual('templates/register.html');
5 });
6 });

Running the tests should confirm that this fails. So next you should add this to the route provider section of js/main.js:

1 .when('/register', {
2 templateUrl: 'templates/register.html',
3 controller: 'RegisterCtrl'
4 })

We also need to allow the register path to be accessed when not logged in:

1.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
2 $rootScope.$on('$routeChangeStart', function (event) {
3
4 if (!Auth.isLoggedIn()) {
5 if ($location.path() !== '/login' && $location.path() !== '/register') {
6 $location.path('/login');
7 }
8 }
9 });
10}])

Our next step is to create a service representing the User endpoint. Here's the test for it:

1 describe('User service', function () {
2 var mockBackend, User;
3
4 beforeEach(inject(function (_User_, _$httpBackend_) {
5 User = _User_;
6 mockBackend = _$httpBackend_;
7 }));
8
9 it('can create a new user', function () {
10 mockBackend.expectPOST('http://localhost:8000/api/users', '{"email":"bob@example.com","name":"bobsmith","password":"password","password_confirmation":"password"}').respond({token: 'mytoken'});
11 var user = new User({
12 email: 'bob@example.com',
13 name: 'bobsmith',
14 password: 'password',
15 password_confirmation: 'password'
16 });
17 user.$save(function (response) {
18 expect(response).toEqualData({token: 'mytoken'});
19 });
20 mockBackend.flush();
21 });
22 });

We're only interested in using this model to create new users at this point, so this is the full scope of this test for now. Make sure the test fails, then we're ready to create the new service in js/services.js:

1.factory('User', function ($resource) {
2 return $resource('http://localhost:8000/api/users/:id', null, {
3 'update': { method: 'PATCH' }
4 });
5})

Note that angular-resource does not support the PUT or PATCH methods by default, but as shown here it's easy to implement it ourselves. That should be sufficient to make our test pass.

Next, we need to create the controller for registration. Here's the test for it:

1 describe('Register Controller', function () {
2 var mockBackend, scope;
3
4 beforeEach(inject(function ($rootScope, $controller, _$httpBackend_) {
5 mockBackend = _$httpBackend_;
6 scope = $rootScope.$new();
7 $controller('RegisterCtrl', {
8 $scope: scope
9 });
10 }));
11
12 // Test controller scope is defined
13 it('should define the scope', function () {
14 expect(scope).toBeDefined();
15 });
16
17 // Test doRegister is defined
18 it('should define the register method', function () {
19 expect(scope.doRegister).toBeDefined();
20 });
21
22 // Test doRegister works
23 it('should allow the user to register', function () {
24 // Mock the backend
25 mockBackend.expectPOST('http://localhost:8000/api/users', '{"email":"user@example.com","name":"bobsmith","password":"password","password_confirmation":"password"}').respond({token: 123});
26
27 // Define login data
28 scope.credentials = {
29 email: 'user@example.com',
30 name: "bobsmith",
31 password: 'password',
32 password_confirmation: 'password'
33 };
34
35 // Submit the request
36 scope.doRegister();
37
38 // Flush the backend
39 mockBackend.flush();
40
41 // Check login complete
42 expect(localStorage.getItem('authHeader')).toEqual('Bearer 123');
43 });
44 });

Make sure the test fails before proceeding. Our RegisterCtrl is very similar to the login controller:

1.controller('RegisterCtrl', function ($scope, $location, User, Auth) {
2 $scope.doRegister = function () {
3 var user = new User($scope.credentials);
4 user.$save(function (response) {
5 if (response.token) {
6 // Set up auth service
7 Auth.setUser(response.token);
8
9 // Redirect
10 $location.path('/');
11 }
12 }, function (err) {
13 alert('Unable to log in - please check your details are correct');
14 });
15 };
16})

Check the tests pass,and we're ready to move on to creating our HTML template. Save this as www/templates/register.html:

1<md-content md-theme="default" layout-gt-sm="row" layout-padding>
2 <div>
3 <md-input-container class="md-block">
4 <label>Email</label>
5 <input ng-model="credentials.email" type="email">
6 </md-input-container>
7
8 <md-input-container class="md-block">
9 <label>Username</label>
10 <input ng-model="credentials.name" type="text">
11 </md-input-container>
12
13 <md-input-container class="md-block">
14 <label>Password</label>
15 <input ng-model="credentials.password" type="password">
16 </md-input-container>
17
18 <md-input-container class="md-block">
19 <label>Confirm Password</label>
20 <input ng-model="credentials.password_confirmation" type="password">
21 </md-input-container>
22
23 <md-button class="md-raised md-primary" ng-click="doRegister()">Submit</md-button>
24 <md-button class="md-raised md-primary" href="/login">Log in</md-button>
25 </div>
26</md-content>

It's very similar to our login template. Speaking of which, we need to add a link to this route there:

1<md-content md-theme="default" layout-gt-sm="row" layout-padding>
2 <div>
3 <md-input-container class="md-block">
4 <label>Email</label>
5 <input ng-model="credentials.email" type="email" />
6 </md-input-container>
7
8 <md-input-container class="md-block">
9 <label>Password</label>
10 <input ng-model="credentials.password" type="password" />
11 </md-input-container>
12 <md-button class="md-raised md-primary" ng-click="doLogin()">Submit</md-button>
13 <md-button class="md-raised md-primary" href="register">Register</md-button>
14 </div>
15</md-content>

With that done, you should now be able to run the Gulp server for the app with gulp and the Laravel backend with php artisan serve and create a new user account.

Adding pets to the home page

Our final task for this lesson is to display a list of pets on the home page. Later we'll refine that functionality, but for now we'll just get a list of all current pets and display them. First we need to write a test for our Pet service:

1 describe('Pet service', function () {
2 var mockBackend, Pet;
3
4 beforeEach(inject(function (_Pet_, _$httpBackend_) {
5 Pet = _Pet_;
6 mockBackend = _$httpBackend_;
7 }));
8
9 it('can fetch pets', function () {
10 mockBackend.expectGET('http://localhost:8000/api/pets').respond([{id:1,name:"Freddie",type:"Cat"}]);
11 expect(Pet).toBeDefined();
12 var pets = Pet.query();
13 mockBackend.flush();
14 expect(pets).toEqualData([{id: 1,name:"Freddie",type:"Cat"}]);
15 });
16 });

Once you know that fails, it's time to implement the service:

1.factory('Pet', function ($resource) {
2 return $resource('http://localhost:8000/api/pets/:id', null, {
3 'update': { method: 'PATCH' }
4 });
5})

Next, we want to add the pets to the scope of the home controller. Amend the test for it as follows:

1 describe('Home Controller', function () {
2 var pets, scope;
3
4 beforeEach(inject(function ($rootScope, $controller, Pet) {
5 pets = Pet;
6 scope = $rootScope.$new();
7 $controller('HomeCtrl', {
8 $scope: scope,
9 pets: [{id:1},{id:2}]
10 });
11 }));
12
13 // Test controller scope is defined
14 it('should define the scope', function () {
15 expect(scope).toBeDefined();
16 });
17
18 // Test pets
19 it('should define the pets', function () {
20 expect(scope.pets).toEqualData([{id: 1}, {id: 2}]);
21 });
22 });

We check to see if the scope contains the pets variable. Once you have a failing test, amend the home controller as follows:

1.controller('HomeCtrl', function ($scope, Pet, pets) {
2 $scope.pets = pets;
3});

We could fetch the via AJAX inside the controller, but there's a better way. We'll create a loader for the pet data and have it resolve that before the page is displayed. To do so, first we need to add the loader service to js/services.js:

1.factory('PetsLoader', ['Pet', '$q', function (Pet, $q) {
2 return function () {
3 var delay = $q.defer();
4 Pet.query(function (response) {
5 delay.resolve(response);
6 }, function () {
7 delay.reject('Unable to fetch pets');
8 });
9 return delay.promise;
10 };
11}])

Then we set that route up to resolve it in js/main.js:

1 .when('/', {
2 templateUrl: 'templates/home.html',
3 controller: 'HomeCtrl',
4 resolve: {
5 pets: ['PetsLoader', function (PetsLoader) {
6 return PetsLoader();
7 }]
8 }
9 })

Now, when we load that route, it will first of all fetch those pets and populate $scope.pets with them.

Now, we need to have some pets in the database, so we'll make a seeder for it. Head back to the backend and run this command:

$ php artisan make:seeder PetTableSeeder

Then amend the file at database/seeds/PetTableSeeder.php as follows:

1<?php
2
3use Illuminate\Database\Seeder;
4use Carbon\Carbon;
5
6class PetTableSeeder extends Seeder
7{
8 /**
9 * Run the database seeds.
10 *
11 * @return void
12 */
13 public function run()
14 {
15 // Add Pets
16 DB::table('pets')->insert([[
17 'name' => 'Freddie',
18 'type' => 'Cat',
19 'available' => 1,
20 'picture' => 'https://placekitten.com/300/300',
21 'created_at' => Carbon::now(),
22 'updated_at' => Carbon::now(),
23 ], [
24 'name' => 'Sophie',
25 'type' => 'Cat',
26 'available' => 1,
27 'picture' => 'https://placekitten.com/300/300',
28 'created_at' => Carbon::now(),
29 'updated_at' => Carbon::now(),
30 ]]);
31 }
32}

And we need to update database/seeds/DatabaseSeeder.php to call our seeder:

1<?php
2
3use Illuminate\Database\Seeder;
4
5class DatabaseSeeder extends Seeder
6{
7 /**
8 * Run the database seeds.
9 *
10 * @return void
11 */
12 public function run()
13 {
14 $this->call(UserTableSeeder::class);
15 $this->call(PetTableSeeder::class);
16 }
17}

For now we'll use placeholder images, but at a later point our backend will be set up to use images uploaded from the admin. Then we need to refresh our migrations and apply the seeders:

1$ php artisan migrate:refresh
2$ php artisan db:seed

Now we just need to amend our home template to show the pets and we're done for today:

1<md-toolbar>
2 <div class="md-toolbar-tools">
3 <md-button aria-label="Log out" href="/logout">
4 Log out
5 </md-button>
6 </div>
7</md-toolbar>
8
9<div layout="column" flex="grow" layout-align="center stretch">
10 <md-card md-theme="default" ng-repeat="pet in pets">
11 <md-card-title>
12 <md-card-title-text>
13 <span class="md-headline">{{ pet.name }}</span>
14 <span class="md-subhead">{{ pet.type }}</span>
15 </md-card-title-text>
16 </md-card-title>
17 <md-card-content>
18 <img class="md-card-image md-media-lg" ng-src="{{ pet.picture }}"></img>
19 </md-card-content>
20 </md-card>
21</div>

Now we can see our pets in the app.

Wrapping up

That's enough for today - the fact that we can log in and out, register, and view the home page is sufficient as a proof of concept for a client. As usual, the results are on Github, tagged lesson-3.

Next time, we'll concentrate exclusively on the back end. We'll build upon what we already have using Laravel to create a full REST API for our app. In a later instalment, we'll move on to build our admin interface for the staff, before switching back to finish off the app. I hope you'll join me then.