Skip to content

Commit 364f126

Browse files
authored
Merge pull request #162 from Facts-and-Files/feat/cachableResponse
feat: cacheable middleware
2 parents 9abfded + 77233ff commit 364f126

File tree

14 files changed

+610
-490
lines changed

14 files changed

+610
-490
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ There are two docker containers/images required.
1515
* [TP-MySQL](https://github.com/Facts-and-Files/tp-mysql)
1616
The MySQL docker container which will provide the database (the dump will be provided on demand).
1717
* [trenc-php](https://github.com/trenc/trenc-php)
18-
An custom PHP image. When bulding the image use the name and tag as it is referenced in the docker-compose.yml here (scto-php:8.0).
18+
An custom PHP image. When bulding the image use the name and tag as it is referenced in the docker-compose.yml here (scto-php:8.1).
1919

2020
## Development/Helpers
2121

src/app/Http/Controllers/PlaceController.php

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
use App\Http\Controllers\ResponseController;
77
use App\Http\Resources\PlaceResource;
88
use App\Models\Place;
9-
use App\Models\PlaceCombinedDetails;
9+
use App\Models\Project;
10+
use App\Models\Story;
11+
use App\Models\Item;
12+
use Illuminate\Database\Eloquent\Builder;
1013
use Illuminate\Http\JsonResponse;
1114
use Illuminate\Http\Request;
1215

@@ -22,21 +25,21 @@ public function index(Request $request): JsonResponse
2225
]);
2326

2427
$queryColumns = [
25-
'Name' => 'Name',
26-
'WikidataName' => 'WikidataName',
27-
'WikidataId' => 'WikidataId',
28-
'ItemId' => 'ItemId',
29-
'UserId' => 'UserId',
30-
'StoryId' => 'StoryId',
31-
'ProjectId' => 'ProjectId',
32-
'PlaceRole' => 'PlaceRole',
28+
'Name' => 'Place.Name',
29+
'WikidataName' => 'Place.WikidataName',
30+
'WikidataId' => 'Place.WikidataId',
31+
'ItemId' => 'Place.ItemId',
32+
'UserId' => 'Place.UserId',
33+
'StoryId' => 'Item.StoryId',
34+
'ProjectId' => 'Story.ProjectId',
35+
'PlaceRole' => 'Place.PlaceRole',
3336
];
3437

35-
$initialSortColumn = 'PlaceId';
38+
$initialSortColumn = 'Place.PlaceId';
3639

37-
$model = new PlaceCombinedDetails();
40+
$query = $this->buildQueryByParentId($request);
3841

39-
$data = $this->getDataByRequest($request, $model, $queryColumns, $initialSortColumn);
42+
$data = $this->getDataByRequest($request, $query, $queryColumns, $initialSortColumn);
4043

4144
if (!$data) {
4245
return $this->sendError('Invalid data', $request . ' not valid', 400);
@@ -117,4 +120,33 @@ public function showByProjectId(Request $request, int $projectId): JsonResponse
117120

118121
return $this->index($request);
119122
}
123+
124+
private function buildQueryByParentId(Request $request): Builder
125+
{
126+
$query = Place::query()
127+
->join('Item', 'Place.ItemId', '=', 'Item.ItemId')
128+
->select('Place.*', 'Item.Title as ItemTitle');
129+
130+
131+
if ($request->has('ProjectId')) {
132+
$projectId = $request['ProjectId'];
133+
Project::findOrFail($projectId);
134+
$query->join('Story', 'Item.StoryId', '=', 'Story.StoryId')
135+
->where('Story.ProjectId', '=', $projectId);
136+
}
137+
138+
if ($request->has('StoryId')) {
139+
$storyId = $request['StoryId'];
140+
Story::findOrFail($storyId);
141+
$query->where('Item.StoryId', '=', $storyId);
142+
}
143+
144+
if ($request->has('ItemId')) {
145+
$itemId = $request['ItemId'];
146+
Item::findOrFail($itemId);
147+
$query->where('Place.ItemId', '=', $itemId);
148+
}
149+
150+
return $query;
151+
}
120152
}

src/app/Http/Controllers/ResponseController.php

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static function sendError(
7373

7474
protected function getDataByRequest(
7575
Request $request,
76-
Model $model,
76+
Model|Builder $modelOrBuilder,
7777
array $queryColumns,
7878
string $initialSortColumn
7979
): Collection
@@ -86,95 +86,98 @@ protected function getDataByRequest(
8686

8787
$sep = $queries['separator'] ?? false;
8888

89-
$data = $model->query();
89+
// Normalize to Builder
90+
$query = $modelOrBuilder instanceof Model
91+
? $modelOrBuilder->newQuery()
92+
: $modelOrBuilder;
9093

9194
foreach ($queries as $queryName => $queryValue) {
9295
if (array_key_exists($queryName, $queryColumns)) {
9396
if ($sep) {
9497
$queryValueArray = array_map('trim', explode($sep, $queryValue));
95-
$data->whereIn($queryColumns[$queryName], $queryValueArray);
98+
$query->whereIn($queryColumns[$queryName], $queryValueArray);
9699
continue;
97100
}
98101

99102
if ($broadMatch === false) {
100-
$data->where($queryColumns[$queryName], $queryValue);
103+
$query->where($queryColumns[$queryName], $queryValue);
101104
} else {
102-
$data->where($queryColumns[$queryName], 'LIKE', '%' . $queryValue . '%');
105+
$query->where($queryColumns[$queryName], 'LIKE', '%' . $queryValue . '%');
103106
}
104107
}
105108
}
106109

107-
$data = $this->filterDataByAreaCoordinates($data, $queries);
110+
$query = $this->filterDataByAreaCoordinates($query, $queries);
108111

109-
$data = $this->filterDataByDateTime($data, $queries);
112+
$query = $this->filterDataByDateTime($query, $queries);
110113

111-
$data = $this->filterDataByQueries($data, $queries, $initialSortColumn);
114+
$query = $this->filterDataByQueries($query, $queries, $initialSortColumn);
112115

113-
return $data;
116+
return $query->get();
114117
}
115118

116-
protected function filterDataByDateTime(Builder $data, array $queries): Builder
119+
protected function filterDataByDateTime(Builder $query, array $queries): Builder
117120
{
118121
$to = isset($queries['to']) ? Carbon::parse($queries['to']) : null;
119122
$from = isset($queries['from']) ? Carbon::parse($queries['from']) : null;
120123
$timeColumn = $queries['timeColumn'] ?? 'Timestamp';
121124

122125
if ($from && $to) {
123-
$data->whereBetween($timeColumn, [$from, $to]);
126+
$query->whereBetween($timeColumn, [$from, $to]);
124127
} elseif ($from) {
125-
$data->where($timeColumn, '>=', $from);
128+
$query->where($timeColumn, '>=', $from);
126129
} elseif ($to) {
127-
$data->where($timeColumn, '<=', $to);
130+
$query->where($timeColumn, '<=', $to);
128131
}
129132

130-
return $data;
133+
return $query;
131134
}
132135

133-
protected function filterDataByAreaCoordinates(Builder $data, array $queries): Builder
136+
protected function filterDataByAreaCoordinates(Builder $query, array $queries): Builder
134137
{
135138
$areaBounds = ['latMin', 'latMax', 'lngMin', 'lngMax'];
136139

137140
$queries = collect($queries);
138141

139142
if (!$queries->has($areaBounds)) {
140-
return $data;
143+
return $query;
141144
}
142145

143146
if ($queries->only($areaBounds)->count() !== 4) {
144-
return $data;
147+
return $query;
145148
}
146149

147-
return $data->whereBetween('Latitude', [$queries['latMin'], $queries['latMax']])
150+
return $query->whereBetween('Latitude', [$queries['latMin'], $queries['latMax']])
148151
->whereBetween('Longitude', [$queries['lngMin'], $queries['lngMax']]);
149152
}
150153

151154
protected function filterDataByQueries(
152-
Builder $data,
155+
Builder $query,
153156
array $queries,
154157
string $initialSortColumn
155-
): Collection
158+
): Builder
156159
{
157160
$limit = $queries['limit'] ?? 100;
158161
$page = $queries['page'] ?? 1;
159162
$orderBy = $queries['orderBy'] ?? $initialSortColumn;
160163
$orderBy = $orderBy === 'id' ? $initialSortColumn : $orderBy;
161164
$orderDir = $queries['orderDir'] ?? 'asc';
162165
$offset = $limit * ($page - 1);
166+
$count = $query->count();
163167

164168
$this->meta = [
165169
'limit' => (int) $limit,
166170
'currentPage' => (int) $page,
167-
'lastPage' => ceil($data->count() / $limit),
171+
'lastPage' => ceil($count / $limit),
168172
'fromEntry' => ($page - 1) * $limit + 1,
169-
'toEntry' => min($page * $limit, $data->count()),
170-
'totalEntries' => $data->count()
173+
'toEntry' => min($page * $limit, $count),
174+
'totalEntries' => $count,
171175
];
172176

173-
$filtered = $data
177+
$filtered = $query
174178
->limit($limit)
175179
->offset($offset)
176-
->orderBy($orderBy, $orderDir)
177-
->get();
180+
->orderBy($orderBy, $orderDir);
178181

179182
return $filtered;
180183
}

src/app/Http/Controllers/TranscriptionController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ public function store(Request $request): JsonResponse
5757

5858
$data->fill($request->all());
5959

60+
$data->Text ??= '';
61+
$data->TextNoTags ??= '';
62+
6063
$data->CurrentVersion = true;
6164
$data->save();
6265

63-
if (is_array($request['Language'])) {
66+
if (isset($request['Language']) && is_array($request['Language'])) {
6467
$data->language()->sync($request['Language']);
6568
}
6669

src/app/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ class Kernel extends HttpKernel
6363
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
6464
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
6565
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
66+
'cacheable' => \App\Http\Middleware\CacheableResponse::class,
6667
];
6768
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Http\JsonResponse;
9+
10+
class CacheableResponse
11+
{
12+
public function handle(Request $request, Closure $next, int|string $ttl = '1h'): JsonResponse
13+
{
14+
$seconds = match($ttl) {
15+
'5m' => 300,
16+
'30m' => 1800,
17+
'1h' => 3600,
18+
'2h' => 7200,
19+
'1d' => 86400,
20+
'1w' => 608400,
21+
'1m' => 2592000,
22+
default => (int) $ttl
23+
};
24+
25+
$cacheKey = $this->generateCacheKey($request);
26+
$forceFresh = $request->boolean('fresh', false);
27+
28+
if ($forceFresh) {
29+
Cache::forget($cacheKey);
30+
}
31+
32+
return Cache::remember($cacheKey, $seconds, function () use ($next, $request) {
33+
return $next($request);
34+
});
35+
}
36+
37+
private function generateCacheKey(Request $request): string
38+
{
39+
$keyData = [
40+
'path' => $request->path(),
41+
'params' => $request->except(['fresh']),
42+
];
43+
44+
return 'api_cache_' . md5(json_encode($keyData));
45+
}
46+
}

src/app/Models/PlaceCombinedDetails.php

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)