Building a Phonegap App with Laravel and Angular - Part 4

Published by at 13th November 2016 4:15 pm

In this instalment we'll return to the back end. What we've done so far is typical of the kind of proof of concept we might do for a client early on, before going back and implementing the full set of features later on. Now we'll go back and start to improve on that rather quick-and-dirty API by making sure we follow a few best practices.

For those of you who want to follow the Laravel Phonegap tutorials, I've created a dedicated category here for those tutorials. This category include RSS and Atom feeds, so if you only want to read those posts, you can do so. I've also done the same for the Django tutorials.

The Repository pattern

One of the issues we currently have with our API is that we're passing our Eloquent models into our controllers. This may not seem like a huge issue, but it means that our controllers are tightly coupled to the Eloquent ORM, so if we wanted to switch to another ORM, or to a completely different database such as MongoDB, we'd have to amend our controllers. That's not good.

However, using the Repository pattern we can first of all define an interface for our repository, and then create a repository class that implements that interface. That way we can interact with the repository class in our controllers, rather than using Eloquent models directly. Then, if we want to switch databases, we merely amend the repository class to change the implementation of those methods, without having to touch our controllers. Also, it makes it much easier to test our controllers in isolation, because we can easily mock our repository class using Mockery and hard-code the response, so our tests won't touch the database and will therefore run more quickly. We won't touch on that this time, but it's a very significant advantage.

If you haven't used interfaces before in PHP, they aren't that hard. They merely specify what methods an object implementing that method must have and what arguments they must accept, but do not specify the details of the implementation. This makes it easy to determine if a class implements an interface correctly, because it will throw an exception if it doesn't.

1<?php
2
3namespace AnimalFriend\Repositories\Interfaces;
4
5interface PetRepositoryInterface {
6 public function all();
7
8 public function findOrFail($id);
9
10 public function create($input);
11}

That's all there is to it. We define it using the interface keyword and we specify the methods it must implement. Save this file at app/Repositories/Interfaces/PetRepositoryInterface.php.

Next, we implement the repository class:

1<?php
2
3namespace AnimalFriend\Repositories;
4
5use AnimalFriend\Pet;
6use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface;
7
8class EloquentPetRepository implements PetRepositoryInterface {
9
10 private $pet;
11
12 public function __construct(Pet $pet)
13 {
14 $this->pet = $pet;
15 }
16
17 public function all()
18 {
19 return $this->pet->all();
20 }
21
22 public function findOrFail($id)
23 {
24 return $this->pet->findOrFail($id);
25 }
26
27 public function create($input)
28 {
29 return $this->pet->create($input);
30 }
31}

Save this to app/Repositories/EloquentPetRepository.php. Note how the methods closely mirror the underlying Eloquent methods, but they don't need to - you could change the underlying implementation of each method, but the repository would still work in exactly the same way.

To make this work, we need to make a few changes elsewhere. In composer.json, we need to add the new Repositories folder to our classmap:

1 "autoload": {
2 "classmap": [
3 "database",
4 "app/Repositories"
5 ],
6 "psr-4": {
7 "AnimalFriend\\": "app/"
8 }
9 },

And in app/Providers/AppServiceProvider.php, we need to bind our new files:

1<?php
2
3namespace AnimalFriend\Providers;
4
5use Illuminate\Support\ServiceProvider;
6
7class AppServiceProvider extends ServiceProvider
8{
9 /**
10 * Bootstrap any application services.
11 *
12 * @return void
13 */
14 public function boot()
15 {
16 //
17 }
18
19 /**
20 * Register any application services.
21 *
22 * @return void
23 */
24 public function register()
25 {
26 $this->app->bind(
27 'AnimalFriend\Repositories\Interfaces\PetRepositoryInterface',
28 'AnimalFriend\Repositories\EloquentPetRepository'
29 );
30 }
31}

With that done, we can now update app/Http/Controllers/PetController.php to use the repository:

1<?php
2
3namespace AnimalFriend\Http\Controllers;
4
5use Illuminate\Http\Request;
6
7use AnimalFriend\Http\Requests;
8use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface as Pet;
9
10class PetController extends Controller
11{
12 private $pet;
13
14 public function __construct(Pet $pet) {
15 $this->pet = $pet;
16 }
17
18 /**
19 * Display a listing of the resource.
20 *
21 * @return \Illuminate\Http\Response
22 */
23 public function index()
24 {
25 // Get all pets
26 $pets = $this->pet->all();
27
28 // Send response
29 return response()->json($pets, 200);
30 }
31
32 /**
33 * Show the form for creating a new resource.
34 *
35 * @return \Illuminate\Http\Response
36 */
37 public function create()
38 {
39 //
40 }
41
42 /**
43 * Store a newly created resource in storage.
44 *
45 * @param \Illuminate\Http\Request $request
46 * @return \Illuminate\Http\Response
47 */
48 public function store(Request $request)
49 {
50 //
51 }
52
53 /**
54 * Display the specified resource.
55 *
56 * @param int $id
57 * @return \Illuminate\Http\Response
58 */
59 public function show($id)
60 {
61 // Get pet
62 $pet = $this->pet->findOrFail($id);
63
64 // Send response
65 return response()->json($pet, 200);
66 }
67
68 /**
69 * Show the form for editing the specified resource.
70 *
71 * @param int $id
72 * @return \Illuminate\Http\Response
73 */
74 public function edit($id)
75 {
76 //
77 }
78
79 /**
80 * Update the specified resource in storage.
81 *
82 * @param \Illuminate\Http\Request $request
83 * @param int $id
84 * @return \Illuminate\Http\Response
85 */
86 public function update(Request $request, $id)
87 {
88 //
89 }
90
91 /**
92 * Remove the specified resource from storage.
93 *
94 * @param int $id
95 * @return \Illuminate\Http\Response
96 */
97 public function destroy($id)
98 {
99 //
100 }
101}

Our repository is now injected automatically into the controller. To make this work we need to run the following command:

$ composer dump-autoload

Running our tests should confirm that everything is still working:

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

Let's do the same for the User model. First we implement our interface in app/Repositories/Interfaces/UserRepositoryInterface.php:

1<?php
2
3namespace AnimalFriend\Repositories\Interfaces;
4
5interface UserRepositoryInterface {
6 public function all();
7
8 public function findOrFail($id);
9
10 public function create($input);
11}

Next we create our repository at app/Repositories/EloquentUserRepository.php:

1<?php
2
3namespace AnimalFriend\Repositories;
4
5use AnimalFriend\User;
6use AnimalFriend\Repositories\Interfaces\UserRepositoryInterface;
7use JWTAuth;
8use Hash;
9
10class EloquentUserRepository implements UserRepositoryInterface {
11
12 private $user;
13
14 public function __construct(User $user)
15 {
16 $this->user = $user;
17 }
18
19 public function all()
20 {
21 return $this->user->all();
22 }
23
24 public function findOrFail($id)
25 {
26 return $this->user->findOrFail($id);
27 }
28
29 public function create($input)
30 {
31 $user = new $this->user;
32 $user->email = $input['email'];
33 $user->name = $input['name'];
34 $user->password = Hash::make($input['password']);
35 $user->save();
36
37 // Create token
38 return JWTAuth::fromUser($user);
39 }
40}

Note how we've moved much of the logic for creating a user into the create() method, and we return the token, not the user model. This makes sense as right now we only ever want to get a token back when we create a user. Later that may change, but there's nothing stopping us adding a new method to implement that behaviour alongside this.

Then we update app/Http/Controllers/UserController.php to use our repository:

1<?php
2
3namespace AnimalFriend\Http\Controllers;
4
5use Illuminate\Http\Request;
6
7use AnimalFriend\Http\Requests;
8use AnimalFriend\Repositories\Interfaces\UserRepositoryInterface as 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 token
56 $token = $this->user->create($request->only(
57 'email',
58 'name',
59 'password'
60 ));
61
62 // Send response
63 return response()->json(['token' => $token], 201);
64 }
65
66 /**
67 * Display the specified resource.
68 *
69 * @param int $id
70 * @return \Illuminate\Http\Response
71 */
72 public function show($id)
73 {
74 //
75 }
76
77 /**
78 * Show the form for editing the specified resource.
79 *
80 * @param int $id
81 * @return \Illuminate\Http\Response
82 */
83 public function edit($id)
84 {
85 //
86 }
87
88 /**
89 * Update the specified resource in storage.
90 *
91 * @param \Illuminate\Http\Request $request
92 * @param int $id
93 * @return \Illuminate\Http\Response
94 */
95 public function update(Request $request, $id)
96 {
97 //
98 }
99
100 /**
101 * Remove the specified resource from storage.
102 *
103 * @param int $id
104 * @return \Illuminate\Http\Response
105 */
106 public function destroy($id)
107 {
108 //
109 }
110}

And add a new binding in app/Providers/AppServiceProvider.php:

1<?php
2
3namespace AnimalFriend\Providers;
4
5use Illuminate\Support\ServiceProvider;
6
7class AppServiceProvider extends ServiceProvider
8{
9 /**
10 * Bootstrap any application services.
11 *
12 * @return void
13 */
14 public function boot()
15 {
16 //
17 }
18
19 /**
20 * Register any application services.
21 *
22 * @return void
23 */
24 public function register()
25 {
26 $this->app->bind(
27 'AnimalFriend\Repositories\Interfaces\PetRepositoryInterface',
28 'AnimalFriend\Repositories\EloquentPetRepository'
29 );
30 $this->app->bind(
31 'AnimalFriend\Repositories\Interfaces\UserRepositoryInterface',
32 'AnimalFriend\Repositories\EloquentUserRepository'
33 );
34 }
35}

Note that we bind the two sets separately - this allows Laravel to figure out which one maps to which.

Let's run our tests to make sure nothing is broken:

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

Now that we've got our repositories in place, we're no longer tightly coupled to Eloquent, and have a more flexible implementation which is easier to test.

Separating our models from our JSON with Fractal

Another problem with our API is that our representation of our data is tightly coupled to our underlying implementation of our models. We therefore can't change our models without potentially changing the data returned by the API. We need to separate our representation of our data from our actual model so that we can more easily specify the exact data we want to return, regardless of the underlying database structure.

Enter Fractal. From the website:

Fractal provides a presentation and transformation layer for complex data output, the like found in RESTful APIs, and works really well with JSON. Think of this as a view layer for your JSON/YAML/etc.

In other words, Fractal lets you specify the format your data will take in one place so that it's easier to return that data in a desired format. We'll use Fractal to specify how we want our API responses to be formatted.

Install Fractal with the following command:

$ composer require league/fractal

Then amend the classmap in composer.json:

1 "autoload": {
2 "classmap": [
3 "database",
4 "app/Repositories",
5 "app/Transformers"
6 ],
7 "psr-4": {
8 "AnimalFriend\\": "app/"
9 }
10 },

Then create the folder app/Transformers and run composer dump-autoload. We're now ready to write our first transformer. Save this as app/Transformers/PetTransformer.php:

1<?php
2
3namespace AnimalFriend\Transformers;
4
5use AnimalFriend\Pet;
6use League\Fractal;
7
8class PetTransformer extends Fractal\TransformerAbstract
9{
10 public function transform(Pet $pet)
11 {
12 return [
13 'id' => (int) $pet->id,
14 'name' => (string) $pet->name,
15 'type' => (string) $pet->type,
16 'available' => (bool) $pet->available,
17 'picture' => (string) $pet->picture
18 ];
19 }
20}

The transform method specifies how we want to represent our objects with our API. We can return only those attributes we want to expose, and amend the structure as we see fit. We could easily represent relations in whatever manner we see fit, whereas before we needed to amend our queries to return the data in the right format, which would potentially be cumbersome.

Now let's amend PetController.php to use this:

1<?php
2
3namespace AnimalFriend\Http\Controllers;
4
5use Illuminate\Http\Request;
6
7use AnimalFriend\Http\Requests;
8use AnimalFriend\Repositories\Interfaces\PetRepositoryInterface as Pet;
9use AnimalFriend\Transformers\PetTransformer;
10use League\Fractal;
11use League\Fractal\Manager;
12
13class PetController extends Controller
14{
15 private $pet, $fractal;
16
17 public function __construct(Pet $pet, Manager $fractal) {
18 $this->pet = $pet;
19 $this->fractal = $fractal;
20 }
21
22 /**
23 * Display a listing of the resource.
24 *
25 * @return \Illuminate\Http\Response
26 */
27 public function index()
28 {
29 // Get all pets
30 $pets = $this->pet->all();
31
32 // Format it
33 $resource = new Fractal\Resource\Collection($pets, new PetTransformer);
34 $data = $this->fractal->createData($resource)->toArray();
35
36 // Send response
37 return response()->json($data, 200);
38 }
39
40 /**
41 * Show the form for creating a new resource.
42 *
43 * @return \Illuminate\Http\Response
44 */
45 public function create()
46 {
47 //
48 }
49
50 /**
51 * Store a newly created resource in storage.
52 *
53 * @param \Illuminate\Http\Request $request
54 * @return \Illuminate\Http\Response
55 */
56 public function store(Request $request)
57 {
58 //
59 }
60
61 /**
62 * Display the specified resource.
63 *
64 * @param int $id
65 * @return \Illuminate\Http\Response
66 */
67 public function show($id)
68 {
69 // Get pet
70 $pet = $this->pet->findOrFail($id);
71
72 // Format it
73 $resource = new Fractal\Resource\Item($pet, new PetTransformer);
74 $data = $this->fractal->createData($resource)->toArray();
75
76 // Send response
77 return response()->json($data, 200);
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}

Note that by default, Fractal places our data inside a dedicated data namespace. This is good because it leaves a place for us to put metadata such as pagination links, but it does mean our controller test has been broken. Let's fix it:

1<?php
2
3use Illuminate\Foundation\Testing\DatabaseMigrations;
4
5class PetControllerTest extends TestCase
6{
7 use DatabaseMigrations;
8
9 /**
10 * Test fetching pets when unauthorised
11 *
12 * @return void
13 */
14 public function testFetchingPetsWhenUnauthorised()
15 {
16 // Create a Pet
17 $pet = factory(AnimalFriend\Pet::class)->create([
18 'name' => 'Freddie',
19 'type' => 'Cat',
20 ]);
21 $this->seeInDatabase('pets', ['type' => 'Cat']);
22
23 // Create request
24 $response = $this->call('GET', '/api/pets');
25 $this->assertResponseStatus(400);
26 }
27
28 /**
29 * Test fetching pets when authorised
30 *
31 * @return void
32 */
33 public function testFetchingPets()
34 {
35 // Create a Pet
36 $pet = factory(AnimalFriend\Pet::class)->create([
37 'name' => 'Freddie',
38 'type' => 'Cat',
39 ]);
40 $this->seeInDatabase('pets', ['type' => 'Cat']);
41
42 // Create a User
43 $user = factory(AnimalFriend\User::class)->create([
44 'name' => 'bobsmith',
45 'email' => 'bob@example.com',
46 ]);
47 $this->seeInDatabase('users', ['email' => 'bob@example.com']);
48
49 // Create request
50 $token = JWTAuth::fromUser($user);
51 $headers = array(
52 'Authorization' => 'Bearer '.$token
53 );
54
55 // Send it
56 $this->json('GET', '/api/pets', [], $headers)
57 ->seeJsonStructure([
58 'data' => [
59 '*' => [
60 'id',
61 'name',
62 'type',
63 'available',
64 'picture'
65 ]
66 ]
67 ]);
68 $this->assertResponseStatus(200);
69 }
70
71 /**
72 * Test fetching pet when unauthorised
73 *
74 * @return void
75 */
76 public function testFetchingPetWhenUnauthorised()
77 {
78 // Create a Pet
79 $pet = factory(AnimalFriend\Pet::class)->create([
80 'name' => 'Freddie',
81 'type' => 'Cat',
82 ]);
83 $this->seeInDatabase('pets', ['type' => 'Cat']);
84
85 // Send request
86 $response = $this->call('GET', '/api/pets/'.$pet->id);
87 $this->assertResponseStatus(400);
88 }
89
90 /**
91 * Test fetching pet which does not exist
92 *
93 * @return void
94 */
95 public function testFetchingPetDoesNotExist()
96 {
97 // Create a User
98 $user = factory(AnimalFriend\User::class)->create([
99 'name' => 'bobsmith',
100 'email' => 'bob@example.com',
101 ]);
102 $this->seeInDatabase('users', ['email' => 'bob@example.com']);
103
104 // Create request
105 $token = JWTAuth::fromUser($user);
106 $headers = array(
107 'Authorization' => 'Bearer '.$token
108 );
109
110 // Send it
111 $this->json('GET', '/api/pets/1', [], $headers);
112 $this->assertResponseStatus(404);
113 }
114
115 /**
116 * Test fetching pet when authorised
117 *
118 * @return void
119 */
120 public function testFetchingPet()
121 {
122 // Create a Pet
123 $pet = factory(AnimalFriend\Pet::class)->create([
124 'name' => 'Freddie',
125 'type' => 'Cat',
126 ]);
127 $this->seeInDatabase('pets', ['type' => 'Cat']);
128
129 // Create a User
130 $user = factory(AnimalFriend\User::class)->create([
131 'name' => 'bobsmith',
132 'email' => 'bob@example.com',
133 ]);
134 $this->seeInDatabase('users', ['email' => 'bob@example.com']);
135
136 // Create request
137 $token = JWTAuth::fromUser($user);
138 $headers = array(
139 'Authorization' => 'Bearer '.$token
140 );
141
142 // Send it
143 $this->json('GET', '/api/pets/'.$pet->id, [], $headers)
144 ->seeJsonStructure([
145 'data' => [
146 'id',
147 'name',
148 'type',
149 'available',
150 'picture'
151 ]
152 ]);
153 $this->assertResponseStatus(200);
154 }
155}

We're also going to amend our test settings to use the array backend for the cache, as this does not require any external dependencies, but still allows us to tag our cache keys (I'll cover that in a future instalment). Change the cache settings in phpunit.xml as follows:

<env name="CACHE_DRIVER" value="array"/>

Let's run our tests to make sure everything's fine:

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

At present our User controller doesn't actually return anything, and the auth only ever returns the token, so it's not worth while adding a transformer now.

Wrapping up

That ends this lesson. We haven't added any functionality, but we have improved the design of our API, and we're now ready to develop it further. As usual, the backend repository has been tagged as lesson-4.

Next time we'll start adding the additional functionality we need to our API.