Skip to content

Commit 99f284f

Browse files
save progress
1 parent fb10b5e commit 99f284f

File tree

28 files changed

+1005
-413
lines changed

28 files changed

+1005
-413
lines changed

backend/app/Http/Controllers/UserController.php

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace App\Http\Controllers;
44

5+
use App\Http\Resources\UserCollection;
6+
use App\Http\Resources\UserResource;
57
use Illuminate\Http\Request;
8+
use Carbon\Carbon;
69

710
/*
811
ChatGPT:
@@ -29,8 +32,8 @@ public function index(Request $request)
2932
// Validate the request
3033
$request->validate([
3134
'country' => 'exists:countries,id',
32-
'dateFrom' => 'regex:/^\d+$/|nullable',
33-
'dateTo' => 'regex:/^\d+$/|nullable',
35+
'dateFrom' => 'integer|nullable',
36+
'dateTo' => 'integer|nullable',
3437
'page' => 'integer|min:1',
3538
'perPage' => 'integer|min:1',
3639
]);
@@ -45,33 +48,24 @@ public function index(Request $request)
4548

4649
// Filter the users based on the provided country and date_of_birth range
4750
$usersQuery = \App\Models\User::query()
48-
->withCountryName()
51+
// ->with('country')
4952
->dateRange($request->input('dateFrom'), $request->input('dateTo'))
5053
->country($request->input('country'));
5154

5255
$page = $request->input('page', 1);
5356
$perPage = $request->input('perPage', 10);
5457
$paginated = $usersQuery->paginate($perPage, ['*'], 'page', $page);
55-
// Return the users as a JSON
56-
return response()->json([
57-
'users' => $paginated->items(),
58-
'totalPages' => (0 < $paginated->total()) ? $paginated->lastPage() : 0,
59-
], 200);
58+
59+
return UserCollection::make($paginated);
6060
}
6161

6262
// GET /users/{id} - This endpoint is used to retrieve a specific user from the database. The {id} path parameter should be replaced with the id of the user you want to retrieve.
6363
public function show($id)
6464
{
65-
// Get the user from the database
66-
$user = \App\Models\User::find($id);
67-
// If the user doesn't exist, return a 404 response
68-
if (!$user) {
69-
return response()->json([
70-
'message' => 'User not found',
71-
], 404);
72-
}
73-
// Return the user as a JSON
74-
return response()->json($user, 200);
65+
// Get the user from the database OR fail
66+
return new UserResource(
67+
\App\Models\User::findOrFail($id)
68+
);
7569
}
7670

7771
// PATCH /users/{id} - This endpoint is used to update a specific user in the database via a country. The {id} path parameter should be replaced with the id of the user you want to update and the client should send the updated information in the request body.
@@ -81,38 +75,30 @@ public function update(Request $request, $id)
8175
$request->validate([
8276
'first_name' => 'required|string',
8377
'last_name' => 'required|string',
84-
'date_of_birth' => 'required|date',
85-
'country_id' => 'required|exists:countries,id',
78+
'date_of_birth' => 'required|integer',
79+
'country_name' => 'required|string',
8680
]);
87-
// Get the user from the database
88-
$user = \App\Models\User::find($id);
89-
// If the user doesn't exist, return a 404 response
90-
if (!$user) {
91-
return response()->json([
92-
'message' => 'User not found',
93-
], 404);
94-
}
81+
$user = \App\Models\User::findOrFail($id);
82+
// Get the country from the database
83+
$country = \App\Models\Country::where('name', $request->input('country_name'))->first();
84+
// // Get or create the country
85+
// $country = \App\Models\Country::firstOrCreate([
86+
// 'name' => $request->input('country_name'),
87+
// ]);
9588
// Update the user
9689
$user->first_name = $request->input('first_name');
9790
$user->last_name = $request->input('last_name');
9891
$user->date_of_birth = $request->input('date_of_birth');
99-
$user->country_id = $request->input('country_id');
92+
$user->country_id = $country->id;
10093
$user->save();
101-
// Return the user as a JSON
102-
return response()->json($user, 200);
94+
95+
return new UserResource($user);
10396
}
10497

10598
// DELETE /users/{id} - This endpoint is used to delete a specific user from the database. The {id} path parameter should be replaced with the id of the user you want to delete.
10699
public function destroy($id)
107100
{
108-
// Get the user from the database
109-
$user = \App\Models\User::find($id);
110-
// If the user doesn't exist, return a 404 response
111-
if (!$user) {
112-
return response()->json([
113-
'message' => 'User not found',
114-
], 404);
115-
}
101+
$user = \App\Models\User::findOrFail($id);
116102
// Delete the user
117103
$user->delete();
118104
// Return a 204 response
@@ -126,18 +112,23 @@ public function store(Request $request)
126112
$request->validate([
127113
'first_name' => 'required|string',
128114
'last_name' => 'required|string',
129-
'date_of_birth' => 'required|date',
130-
'country_id' => 'required|exists:countries,id',
115+
'date_of_birth' => 'required|integer',
116+
'country_name' => 'required|string',
117+
]);
118+
119+
$dateOfBirth = Carbon::createFromTimestamp($request->input('date_of_birth'));
120+
// Create the user and country if they don't exist
121+
$country = \App\Models\Country::firstOrCreate([
122+
'name' => $request->input('country_name'),
131123
]);
132-
// Create the user
133124
$user = \App\Models\User::create([
134125
'first_name' => $request->input('first_name'),
135126
'last_name' => $request->input('last_name'),
136-
'date_of_birth' => $request->input('date_of_birth'),
137-
'country_id' => $request->input('country_id'),
127+
'date_of_birth' => $dateOfBirth,
128+
'country_id' => $country->id,
138129
]);
139130
$user->save();
140-
// Return the user as a JSON
141-
return response()->json($user, 201);
131+
132+
return new UserResource($user);
142133
}
143134
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Http\Resources;
4+
5+
use Illuminate\Http\Resources\Json\ResourceCollection;
6+
7+
class UserCollection extends ResourceCollection
8+
{
9+
// wrap as "users"
10+
public static $wrap = 'users';
11+
12+
/**
13+
* Transform the resource collection into an array.
14+
*
15+
* @param \Illuminate\Http\Request $request
16+
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
17+
*/
18+
public function toArray($request)
19+
{
20+
return parent::toArray($request);
21+
}
22+
23+
// paginationInformation override
24+
public function paginationInformation()
25+
{
26+
$totalPages = $this->resource->lastPage();
27+
return [
28+
'totalPages' => (0 < $this->resource->total()) ? $totalPages : 0,
29+
];
30+
}
31+
32+
// load the country relation in constructor to avoid N+1 problem
33+
public function __construct($resource)
34+
{
35+
$resource->loadMissing('country');
36+
parent::__construct($resource);
37+
}
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Http\Resources;
4+
5+
use Illuminate\Http\Resources\Json\JsonResource;
6+
7+
class UserResource extends JsonResource
8+
{
9+
// no wrap
10+
public static $wrap = null;
11+
12+
/**
13+
* Transform the resource into an array.
14+
*
15+
* @param \Illuminate\Http\Request $request
16+
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
17+
*/
18+
public function toArray($request)
19+
{
20+
$result = parent::toArray($request);
21+
22+
// date_of_birth as a unix timestamp
23+
$result['date_of_birth'] = $this->date_of_birth->getTimestamp();
24+
// country_name
25+
$result['country_name'] = $this->country->name;
26+
return $result;
27+
}
28+
}

backend/app/Models/User.php

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ class User extends Model
1818
'country_id',
1919
];
2020

21+
// cast fields
22+
protected $casts = [
23+
'date_of_birth' => 'datetime',
24+
];
25+
2126
public $timestamps = false;
22-
23-
public function country() {
27+
28+
public function country()
29+
{
2430
return $this->belongsTo(Country::class);
2531
}
2632

@@ -49,11 +55,4 @@ public function scopeCountry(Builder $query, $countryId = null)
4955

5056
return $query;
5157
}
52-
53-
public function scopeWithCountryName($query)
54-
{
55-
$tbl = $this->getTable();
56-
return $query->leftJoin('countries', 'countries.id', '=', $tbl . '.country_id')
57-
->addSelect($tbl . '.*', 'countries.name as country_name');
58-
}
5958
}

backend/database/factories/UserFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public function definition()
1616
return [
1717
'first_name' => $this->faker->firstName,
1818
'last_name' => $this->faker->lastName,
19-
// date of birth is a date between 18 and 65 years ago
20-
'date_of_birth' => $this->faker->dateTimeBetween('-65 years', '-18 years'),
19+
// date of birth is a date between 18 and 65 years ago as a unix timestamp rounded to the day
20+
'date_of_birth' => $this->faker->dateTimeBetween('-65 years', '-18 years')->format('Y-m-d'),
2121
// country_id is referenced to the countries table
2222
'country_id' => function () {
2323
return \App\Models\Country::factory()->create()->id;

backend/tests/Unit/Routes/UsersFilterTest.php

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ public function testNoUsers()
1212
{
1313
$response = $this->get('/api/users');
1414
$response->assertHeader('Content-Type', 'application/json');
15-
$response->assertJsonFragment([
15+
$response->assertJson([
1616
'users' => [],
1717
'totalPages' => 0,
1818
]);
1919
}
20-
20+
2121
// Route '/api/users?country=&dateFrom=&dateTo=' should return a JSON with the filtered users
2222
public function testFilterUsers()
2323
{
@@ -113,10 +113,18 @@ private function _setupUsers()
113113
'page' => 1,
114114
'perPage' => 100000,
115115
];
116-
// Add the country_name to the users
117-
$users->each(function ($user) use ($country) {
118-
$user->country_name = $country->name;
119-
});
116+
117+
// convert the users to array with the correct format
118+
$users = $users->map(function ($user) {
119+
return [
120+
'id' => $user->id,
121+
'first_name' => $user->first_name,
122+
'last_name' => $user->last_name,
123+
'country_id' => $user->country_id,
124+
'country_name' => $user->country->name,
125+
'date_of_birth' => $user->date_of_birth->timestamp,
126+
];
127+
})->toArray();
120128
return [$country, $users, $query];
121129
}
122130

@@ -126,8 +134,8 @@ public function testGetUsersWithDefaultParameters()
126134
list($country, $users, $query) = $this->_setupUsers();
127135
// Check that the response is contains the correct data for the filtered user
128136
$this->get('/api/users?' . http_build_query($query))
129-
->assertJsonFragment([
130-
'users' => $users->toArray(),
137+
->assertJson([
138+
'users' => $users,
131139
'totalPages' => 1,
132140
]);
133141
}
@@ -154,16 +162,36 @@ public function testGetUsersAllPagesValid($perPage)
154162
{
155163
list($country, $users, $query) = $this->_setupUsers();
156164
$query['perPage'] = $perPage;
157-
$totalPages = ceil($users->count() / $perPage);
165+
$totalPages = ceil(count($users) / $perPage);
158166
for ($i = 1; $i <= $totalPages; $i++) {
159167
// Send the GET request with the current page
160168
$query['page'] = $i;
161169
$response = $this->get('/api/users?' . http_build_query($query));
162-
$expectedUsers = $users->slice(($i - 1) * $perPage, $perPage)->toArray();
163-
$response->assertJsonFragment([
164-
'users' => array_values($expectedUsers),
165-
'totalPages' => $totalPages,
170+
// take the expected users for the current page
171+
$expectedUsers = array_slice($users, ($i - 1) * $perPage, $perPage);
172+
$response->assertJson([
173+
'users' => $expectedUsers,
174+
'totalPages' => (int)$totalPages,
166175
]);
167176
}
168177
}
178+
179+
// test that didn't hit into N+1 problem with the country relation
180+
public function testGetUsersAllPagesNPlus1()
181+
{
182+
list($country, $users, $query) = $this->_setupUsers();
183+
// Enable the query log
184+
\DB::connection()->enableQueryLog();
185+
186+
// get all users
187+
$query['perPage'] = count($users);
188+
$query['page'] = 1;
189+
$this->get('/api/users?' . http_build_query($query));
190+
191+
// Disable the query log and get the queries
192+
\DB::connection()->disableQueryLog();
193+
$queries = \DB::getQueryLog();
194+
// Check that the number of queries is less than the number of users
195+
$this->assertLessThan(count($users), count($queries));
196+
}
169197
}

0 commit comments

Comments
 (0)