Building a Phonegap app with Laravel and Angular - Part 3
Published by Matthew Daly 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<?php23use Illuminate\Foundation\Testing\DatabaseMigrations;45class UserControllerTest extends TestCase6{7 /**8 * Test creating a user - invalid9 *10 * @return void11 */12 public function testPostingInvalidUser()13 {14 // Create a request15 $data = array(16 'name' => 'Bob Smith',17 'email' => 'bob@example.com'18 );19 $this->json('POST', '/api/users', $data);20 $this->assertResponseStatus(422);21 }2223 /**24 * Test creating a user25 *26 * @return void27 */28 public function testPostingUser()29 {30 // Create a request31 $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']);4041 // Check user exists42 $saved = User::first();43 $this->assertEquals($saved->email, 'bob@example.com');44 $this->assertEquals($saved->name, 'Bob Smith');45 }4647 /**48 * Test creating a duplicate user49 *50 * @return void51 */52 public function testPostingDuplicateUser()53 {54 // Create user55 $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']);6162 // Create a request63 $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/phpunit2PHPUnit 5.5.4 by Sebastian Bergmann and contributors.34........FFF. 12 / 12 (100%)56Time: 827 ms, Memory: 18.00MB78There were 3 failures:9101) UserControllerTest::testPostingInvalidUser11Expected status code 422, got 404.12Failed asserting that 404 matches expected 422.1314/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:64815/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:2116172) UserControllerTest::testPostingUser18Expected status code 201, got 404.19Failed asserting that 404 matches expected 201.2021/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:64822/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:3923243) UserControllerTest::testPostingDuplicateUser25Expected status code 422, got 404.26Failed asserting that 404 matches expected 422.2728/home/matthew/Projects/mynewanimalfriend-backend/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:64829/home/matthew/Projects/mynewanimalfriend-backend/tests/UserControllerTest.php:713031FAILURES!32Tests: 12, Assertions: 43, Failures: 3.
Next, we create our new controller:
$ php artisan make:controller UserController --resource
Let's populate it:
1<?php23namespace AnimalFriend\Http\Controllers;45use Illuminate\Http\Request;67use AnimalFriend\Http\Requests;8use AnimalFriend\User;9use JWTAuth;10use Hash;1112class UserController extends Controller13{14 private $user;1516 public function __construct(User $user) {17 $this->user = $user;18 }1920 /**21 * Display a listing of the resource.22 *23 * @return \Illuminate\Http\Response24 */25 public function index()26 {27 //28 }2930 /**31 * Show the form for creating a new resource.32 *33 * @return \Illuminate\Http\Response34 */35 public function create()36 {37 //38 }3940 /**41 * Store a newly created resource in storage.42 *43 * @param \Illuminate\Http\Request $request44 * @return \Illuminate\Http\Response45 */46 public function store(Request $request)47 {48 // Validate request49 $valid = $this->validate($request, [50 'email' => 'required|email|unique:users,email',51 'name' => 'required|string',52 'password' => 'required|confirmed',53 ]);5455 // Create user56 $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();6162 // Create token63 $token = JWTAuth::fromUser($user);6465 // Send response66 return response()->json(['token' => $token], 201);67 }6869 /**70 * Display the specified resource.71 *72 * @param int $id73 * @return \Illuminate\Http\Response74 */75 public function show($id)76 {77 //78 }7980 /**81 * Show the form for editing the specified resource.82 *83 * @param int $id84 * @return \Illuminate\Http\Response85 */86 public function edit($id)87 {88 //89 }9091 /**92 * Update the specified resource in storage.93 *94 * @param \Illuminate\Http\Request $request95 * @param int $id96 * @return \Illuminate\Http\Response97 */98 public function update(Request $request, $id)99 {100 //101 }102103 /**104 * Remove the specified resource from storage.105 *106 * @param int $id107 * @return \Illuminate\Http\Response108 */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/phpunit2PHPUnit 5.5.4 by Sebastian Bergmann and contributors.34............ 12 / 12 (100%)56Time: 905 ms, Memory: 20.00MB78OK (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) {34 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;34 beforeEach(inject(function (_User_, _$httpBackend_) {5 User = _User_;6 mockBackend = _$httpBackend_;7 }));89 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;34 beforeEach(inject(function ($rootScope, $controller, _$httpBackend_) {5 mockBackend = _$httpBackend_;6 scope = $rootScope.$new();7 $controller('RegisterCtrl', {8 $scope: scope9 });10 }));1112 // Test controller scope is defined13 it('should define the scope', function () {14 expect(scope).toBeDefined();15 });1617 // Test doRegister is defined18 it('should define the register method', function () {19 expect(scope.doRegister).toBeDefined();20 });2122 // Test doRegister works23 it('should allow the user to register', function () {24 // Mock the backend25 mockBackend.expectPOST('http://localhost:8000/api/users', '{"email":"user@example.com","name":"bobsmith","password":"password","password_confirmation":"password"}').respond({token: 123});2627 // Define login data28 scope.credentials = {29 email: 'user@example.com',30 name: "bobsmith",31 password: 'password',32 password_confirmation: 'password'33 };3435 // Submit the request36 scope.doRegister();3738 // Flush the backend39 mockBackend.flush();4041 // Check login complete42 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 service7 Auth.setUser(response.token);89 // Redirect10 $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>78 <md-input-container class="md-block">9 <label>Username</label>10 <input ng-model="credentials.name" type="text">11 </md-input-container>1213 <md-input-container class="md-block">14 <label>Password</label>15 <input ng-model="credentials.password" type="password">16 </md-input-container>1718 <md-input-container class="md-block">19 <label>Confirm Password</label>20 <input ng-model="credentials.password_confirmation" type="password">21 </md-input-container>2223 <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>78 <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;34 beforeEach(inject(function (_Pet_, _$httpBackend_) {5 Pet = _Pet_;6 mockBackend = _$httpBackend_;7 }));89 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;34 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 }));1213 // Test controller scope is defined14 it('should define the scope', function () {15 expect(scope).toBeDefined();16 });1718 // Test pets19 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<?php23use Illuminate\Database\Seeder;4use Carbon\Carbon;56class PetTableSeeder extends Seeder7{8 /**9 * Run the database seeds.10 *11 * @return void12 */13 public function run()14 {15 // Add Pets16 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<?php23use Illuminate\Database\Seeder;45class DatabaseSeeder extends Seeder6{7 /**8 * Run the database seeds.9 *10 * @return void11 */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:refresh2$ 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 out5 </md-button>6 </div>7</md-toolbar>89<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.