Skip to content

Commit 55c7b90

Browse files
author
Admin
committed
Add get update and delete related by identifier
1 parent 32f1132 commit 55c7b90

File tree

2 files changed

+167
-1
lines changed

2 files changed

+167
-1
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ Extend:
6565

6666
Create a Controller that uses ResourceControllerTrait and call $this->init() from its __construct.
6767

68+
Optionally if you don't want to expose some models as resources but, you want to expose them as relation on an exposed resource then define in your Base Controller or ResourceController:
69+
70+
```php
71+
protected array $relatedModelFqnToControllerMap = [
72+
RelatedBaseModelChild::class => RelatedResourceControllerTraitIncludedChild::class,
73+
];
74+
````
75+
6876
For model properties autocomplete:
6977

7078
- extend BaseModelAttributes following the same FQN structure as the parent's:
@@ -141,6 +149,10 @@ Register the crud routes in your application using (for example in Laravel)
141149
Route::get('/' . $resource . '/{identifier}', [$controller, 'get'])->name('apiinfo.get_' . $resource);
142150
Route::delete('/' . $resource . '/{identifier}', [$controller, 'delete'])->name('apiinfo.delete_' . $resource);
143151
// Route::match(['post', 'get'], '/' . $resource . '/{identifier}/{relation}', [$controller, 'listRelation']); // paid version only
152+
Route::get('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [$controller, 'getRelated']);
153+
Route::put('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [$controller, 'updateRelated']);
154+
Route::delete('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [$controller, 'deleteRelated']);
155+
144156
}
145157
} catch (Throwable $e) {
146158
\Illuminate\Support\Facades\Log::error($e->getMessage());
@@ -191,6 +203,19 @@ for example for lumen:
191203
'as' => $resource . '.delete',
192204
'uses' => $controller . '@delete',
193205
]);
206+
207+
$router->get('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [
208+
'as' => $resource . '.getRelated',
209+
'uses' => $controller . '@getRelated',
210+
]);
211+
$router->put('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [
212+
'as' => $resource . '.updateRelated',
213+
'uses' => $controller . '@updateRelated',
214+
]);
215+
$router->delete('/' . $resource . '/{identifier}/{relation}/{relatedIdentifier}', [
216+
'as' => $resource . '.deleteRelated',
217+
'uses' => $controller . '@deleteRelated',
218+
]);
194219
}
195220
} catch (Throwable $e) {
196221
\Illuminate\Support\Facades\Log::error($e->getMessage());
@@ -256,6 +281,8 @@ The above "errors" are optional and appear only for validation errors while "mes
256281
#### III.2 Get resource
257282
**GET** /{resource}/{identifier}?withRelations[]=has_manyRelation&withRelations[]=has_oneRelation&withRelationsCount[]=has_manyRelation&withRelationsExistence[]=has_manyRelation
258283

284+
**GET** /{resource}/{identifier}/{relation}/{relatedIdentifier}?withRelations[]=has_manyRelation&withRelations[]=has_oneRelation&withRelationsCount[]=has_manyRelation&withRelationsExistence[]=has_manyRelation
285+
259286
headers:
260287

261288
Authorization: Bearer ... // if needed. not coded in this lib
@@ -411,6 +438,8 @@ Obs.
411438
#### III.4 Update resource (or create)
412439
**PUT** /{resource}/{identifier}
413440

441+
**PUT** /{resource}/{identifier}/{resource}/{relatedIdentifier}
442+
414443
headers:
415444

416445
Authorization: Bearer ... // if needed. not coded in this lib
@@ -461,6 +490,8 @@ Update will validate only dirty columns, not all sent columns, meaning the updat
461490
#### III.5 Delete resource
462491
**DELETE** /{resource}/{identifier}
463492

493+
**DELETE** /{resource}/{identifier}/{resource}/{relatedIdentifier}
494+
464495
headers:
465496

466497
Authorization: Bearer ... // if needed. not coded in this lib

src/Http/Controllers/ResourceControllerTrait.php

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ trait ResourceControllerTrait
2929
protected string $label = '';
3030
protected bool $simplePaginate = false;
3131
protected array $modelFqnToControllerMap = [];
32+
33+
/**
34+
* If a related resource is not exposed via crud in $modelFqnToControllerMap,
35+
* it can be exposed in this property
36+
*/
37+
protected array $relatedModelFqnToControllerMap = [];
3238
protected ResourceServiceInterface $resourceService;
3339
protected array $paginationKeys = [
3440
'current_page',
@@ -213,6 +219,16 @@ protected function getFilteredRelations(array $relations, ?BaseModel $baseModel
213219
$this->getResourceAsModelFQN())::WITH_RELATIONS, $relations);
214220
}
215221

222+
/**
223+
* @throws \Exception
224+
*/
225+
protected function validateRelation(string $relation): void
226+
{
227+
if (!\in_array($relation, $this->getResourceAsModelFQN()::WITH_RELATIONS, true)) {
228+
throw new \Exception('Relation as subresource not found for this resource.');
229+
}
230+
}
231+
216232
protected function handleList(array $allRequest, Request $request): Response
217233
{
218234
try {
@@ -240,6 +256,74 @@ protected function handleList(array $allRequest, Request $request): Response
240256
}
241257
}
242258

259+
public function getRelated(
260+
Request $request,
261+
string $identifier,
262+
string $relation,
263+
string $relatedIdentifier
264+
): JsonResponse {
265+
try {
266+
return $this->getRelatedController(
267+
$this->getRelatedFromRelation($identifier, $relation, $relatedIdentifier),
268+
$relation
269+
)->get($relatedIdentifier, $request);
270+
} catch (\Throwable $e) {
271+
if (!$e instanceof ModelNotFoundException) {
272+
Log::error($this->label . '/' . $identifier . '/' . $relation . '/'. $relatedIdentifier .
273+
', error = ' . $e->getMessage());
274+
}
275+
276+
return GeneralHelper::app(JsonResponse::class, [
277+
'data' => ['message' => GeneralHelper::getSafeErrorMessage($e)],
278+
'status' => 400
279+
]);
280+
}
281+
}
282+
283+
public function updateRelated(
284+
Request $request,
285+
string $identifier,
286+
string $relation,
287+
string $relatedIdentifier
288+
): JsonResponse {
289+
try {
290+
return $this->getRelatedController(
291+
$this->getRelatedFromRelation($identifier, $relation, $relatedIdentifier),
292+
$relation
293+
)->update($relatedIdentifier, $request);
294+
} catch (\Throwable $e) {
295+
if (!$e instanceof ModelNotFoundException) {
296+
Log::error($this->label . '/' . $identifier . '/' . $relation . '/'. $relatedIdentifier .
297+
', error = ' . $e->getMessage());
298+
}
299+
300+
return GeneralHelper::app(JsonResponse::class, [
301+
'data' => ['message' => GeneralHelper::getSafeErrorMessage($e)],
302+
'status' => 400
303+
]);
304+
}
305+
}
306+
307+
public function deleteRelated(string $identifier, string $relation, string $relatedIdentifier): JsonResponse
308+
{
309+
try {
310+
return $this->getRelatedController(
311+
$this->getRelatedFromRelation($identifier, $relation, $relatedIdentifier),
312+
$relation
313+
)->delete($relatedIdentifier);
314+
} catch (\Throwable $e) {
315+
if (!$e instanceof ModelNotFoundException) {
316+
Log::error($this->label . '/' . $identifier . '/' . $relation . '/'. $relatedIdentifier .
317+
', error = ' . $e->getMessage());
318+
}
319+
320+
return GeneralHelper::app(JsonResponse::class, [
321+
'data' => ['message' => GeneralHelper::getSafeErrorMessage($e)],
322+
'status' => 400
323+
]);
324+
}
325+
}
326+
243327
/**
244328
* @throws \Exception
245329
*/
@@ -255,7 +339,6 @@ protected function getResourceAsModelFQN(): string
255339
throw new \Exception('Could not getResourceAsModelFQN.');
256340
}
257341

258-
259342
protected function getEmptyPaginatedResponse(array $request): JsonResponse
260343
{
261344
$data = [
@@ -379,4 +462,56 @@ protected function throwIfForbidden(bool $condition): void
379462
throw new \Exception('Forbidden');
380463
}
381464
}
465+
466+
/**
467+
* @return self
468+
* @throws \Throwable
469+
*/
470+
protected function getRelatedController(BaseModel $related, string $relation): object
471+
{
472+
$controllerFqn = (string)($this->modelFqnToControllerMap[$related::class] ??
473+
($this->relatedModelFqnToControllerMap[$related::class] ?? ''));
474+
475+
if ('' === $controllerFqn) {
476+
throw new \Exception('Related ' . $relation . ' not exposed as resource.');
477+
}
478+
479+
return GeneralHelper::app($controllerFqn);
480+
}
481+
482+
/**
483+
* @throws \Throwable
484+
*/
485+
protected function getRelatedFromRelation(
486+
string $identifier,
487+
string $relation,
488+
string $relatedIdentifier
489+
): BaseModel {
490+
$this->validateRelation($relation);
491+
/** @var Relation $relationInstance */
492+
$relationInstance = $this->resourceService->get($identifier, appendIndex: false)->{$relation}();
493+
/** @var BaseModel $related */
494+
$related = $relationInstance->getRelated();
495+
$exploded = \explode($related::COMPOSITE_PK_SEPARATOR, $relatedIdentifier);
496+
$pks = [];
497+
498+
foreach ($related->getPrimaryKeyFilter() as $column => $value) {
499+
if (!\is_array($value) && \is_string($column)) {
500+
$pks[] = $column;
501+
502+
continue;
503+
}
504+
505+
$pks[] = \reset($value);
506+
}
507+
508+
if (
509+
\count($pks) !== \count($exploded)
510+
|| !$relationInstance->where(\array_combine($pks, $exploded))->exists()
511+
) {
512+
throw (new ModelNotFoundException())->setModel($related::class);
513+
}
514+
515+
return $related;
516+
}
382517
}

0 commit comments

Comments
 (0)