diff --git a/docs/docs/4.0/repository-pattern/repository-pattern.md b/docs/docs/4.0/repository-pattern/repository-pattern.md index f97dff36a..31af0da1c 100644 --- a/docs/docs/4.0/repository-pattern/repository-pattern.md +++ b/docs/docs/4.0/repository-pattern/repository-pattern.md @@ -817,9 +817,13 @@ eager load a relationship in terms of using it in fields, or whatever else: ```php // UserRepository.php -public static $with = ['posts']; +public static $withs = ['posts']; ``` +:::warn `withs` is not type +Laravel uses the `with` property on models, on repositories we use `$withs`, it's not a typo. +::: + ## Store bulk flow The bulk store means that you can create many entries at once, for example if you have a list of invoice entries, diff --git a/src/Eager/Related.php b/src/Eager/Related.php index 7b713b463..8601bab45 100644 --- a/src/Eager/Related.php +++ b/src/Eager/Related.php @@ -3,16 +3,30 @@ namespace Binaryk\LaravelRestify\Eager; use Binaryk\LaravelRestify\Fields\EagerField; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Repositories\Repository; +use Binaryk\LaravelRestify\Resolvable; use Binaryk\LaravelRestify\Traits\Make; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; use JsonSerializable; -class Related implements JsonSerializable +class Related implements JsonSerializable, Resolvable { use Make; private string $relation; + /** + * This is the default value. + * + * @var callable|string|int + */ + private $value; + private ?EagerField $field; public function __construct(string $relation, EagerField $field = null) @@ -31,11 +45,56 @@ public function getRelation(): string return $this->relation; } + public function getValue() + { + return $this->value; + } + public function resolveField(Repository $repository): EagerField { return $this->field->resolve($repository); } + public function resolve(RestifyRequest $request, Repository $repository): self + { + if (Str::contains($this->getRelation(), '.')) { + $repository->resource->loadMissing($this->getRelation()); + + $key = Str::before($this->getRelation(), '.'); + + $this->value = Arr::get($repository->resource->relationsToArray(), $key); + + return $this; + } + + /** * To avoid circular relationships and deep stack calls, we will do not load eager fields. */ + if ($this->isEager() && $repository->isEagerState() === false) { + $this->value = $this->resolveField($repository)->value; + + return $this; + } + + $paginator = $repository->resource->relationLoaded($this->getRelation()) + ? $repository->resource->{$this->getRelation()} + : $repository->resource->{$this->getRelation()}(); + + switch ($paginator) { + case $paginator instanceof Builder: + $this->value = ($repository::$relatedCast)::fromBuilder($request, $paginator, $repository); + break; + case $paginator instanceof Relation: + $this->value = ($repository::$relatedCast)::fromRelation($request, $paginator, $repository); + break; + case $paginator instanceof Collection: + $this->value = $paginator; + break; + default: + $this->value = $paginator; + } + + return $this; + } + public function jsonSerialize() { return [ diff --git a/src/Fields/Concerns/Attachable.php b/src/Fields/Concerns/Attachable.php index 52c6d7398..10111342b 100644 --- a/src/Fields/Concerns/Attachable.php +++ b/src/Fields/Concerns/Attachable.php @@ -9,6 +9,7 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Arr; +use Illuminate\Validation\ValidationException; trait Attachable { @@ -17,6 +18,11 @@ trait Attachable */ private $canAttachCallback; + /** + * @var Closure + */ + private $validationCallback; + /** * @var Closure */ @@ -140,4 +146,38 @@ public function collectPivotFields(): PivotsCollection { return PivotsCollection::make($this->pivotFields); } + + public function validationCallback(Closure $validationCallback) + { + $this->validationCallback = $validationCallback; + + return $this; + } + + public function validate(RestifyRequest $request, $pivot): bool + { + if (is_callable($this->validationCallback)) { + throw_unless( + call_user_func($this->validationCallback, $request, $pivot), + ValidationException::withMessages([__('Invalid data.')]) + ); + } + + return true; + } + + public function unique(): self + { + $this->validationCallback = function (RestifyRequest $request, $pivot) { + $valid = $this->getRelation($request->repository()) + ->where($pivot->toArray()) + ->count() === 0; + + throw_unless($valid, ValidationException::withMessages([__('Invalid data. The relation must be unique.')])); + + return $valid; + }; + + return $this; + } } diff --git a/src/Repositories/Concerns/InteractsWithAttachers.php b/src/Repositories/Concerns/InteractsWithAttachers.php index 1a8863059..6467bb0d2 100644 --- a/src/Repositories/Concerns/InteractsWithAttachers.php +++ b/src/Repositories/Concerns/InteractsWithAttachers.php @@ -22,10 +22,6 @@ public function authorizeBelongsToMany(RestifyRequest $request): self abort(400, "Missing BelongsToMany or MorphToMany field for [{$request->relatedRepository}]. This field should be in the related of the [{$class}] class. Or you are not authorized to use that repository (see `allowRestify` policy method)."); } - $field->authorizeToAttach( - $request, - ); - return $this; } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index e039f82be..5947ea6b5 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -24,15 +24,12 @@ use Binaryk\LaravelRestify\Traits\InteractWithSearch; use Binaryk\LaravelRestify\Traits\PerformsQueries; use Illuminate\Contracts\Pagination\LengthAwarePaginator; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; use Illuminate\Http\Resources\DelegatesToResource; use Illuminate\Pagination\AbstractPaginator; use Illuminate\Routing\Router; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -575,9 +572,7 @@ public function resolveIndexPivots(RestifyRequest $request): array */ public function resolveRelationships($request): array { - $withs = collect(); - - static::collectRelated() + return static::collectRelated() ->authorized($request) ->inRequest($request) ->when($request->isShowRequest(), function (RelatedCollection $collection) use ($request) { @@ -587,39 +582,9 @@ public function resolveRelationships($request): array return $collection->forIndex($request, $this); }) ->mapIntoRelated($request) - ->each(function (Related $related) use ($request, $withs) { - $relation = $related->getRelation(); - - if (Str::contains($relation, '.')) { - $this->resource->loadMissing($relation); - - return $withs->put($key = Str::before($relation, '.'), Arr::get($this->resource->relationsToArray(), $key)); - } - - /** * To avoid circular relationships and deep stack calls, we will do not load eager fields. */ - if ($related->isEager() && $this->isEagerState() === false) { - return $withs->put($relation, $related->resolveField($this)->value); - } - - $paginator = $this->resource->relationLoaded($relation) - ? $this->resource->{$relation} - : $this->resource->{$relation}(); - - collect([ - Builder::class => fn () => $withs->put($relation, (static::$relatedCast)::fromBuilder($request, $paginator, $this)), - - Relation::class => fn () => $withs->put($relation, (static::$relatedCast)::fromRelation($request, $paginator, $this)), - - Collection::class => fn () => $withs->put($relation, $paginator), - - Model::class => fn () => fn () => $withs->put($relation, $paginator), - - ])->first(fn ($fn, $class) => $paginator instanceof $class, - fn () => fn () => $withs->put($relation, $paginator) - )(); - }); - - return $withs->all(); + ->map(function (Related $related) use ($request) { + return $related->resolve($request, $this)->getValue(); + })->all(); } /** @@ -830,6 +795,8 @@ public function attach(RestifyRequest $request, $repositoryId, Collection $pivot $fields = $eagerField->collectPivotFields()->filter(fn ($pivotField) => $request->has($pivotField->attribute))->values(); $pivots->map(function ($pivot) use ($request, $fields, $eagerField) { + $eagerField->validate($request, $pivot); + static::validatorForAttach($request)->validate(); static::fillFields($request, $pivot, $fields); diff --git a/src/Resolvable.php b/src/Resolvable.php new file mode 100644 index 000000000..5d889ba08 --- /dev/null +++ b/src/Resolvable.php @@ -0,0 +1,11 @@ +repository = $repository; - $query = $this->prepareMatchFields($request, $this->prepareSearchFields($request, $repository::query($request), $this->fixedInput), $this->fixedInput); + $query = $this->prepareMatchFields( + $request, + $this->prepareSearchFields($request, $this->prepareRelations($request, $repository::query($request)), $this->fixedInput), + $this->fixedInput); $query = $this->applyFilters($request, $repository, $query); @@ -118,17 +121,9 @@ public function prepareOrders(RestifyRequest $request, $query) return $query; } - public function prepareRelations(RestifyRequest $request, $query, $extra = []) + public function prepareRelations(RestifyRequest $request, $query) { - $relations = array_merge($extra, explode(',', $request->input('related'))); - - foreach ($relations as $relation) { - if (in_array($relation, $this->repository->getWiths())) { - $query->with($relation); - } - } - - return $query; + return $query->with($this->repository->getWiths()); } public function prepareSearchFields(RestifyRequest $request, $query, $extra = []) diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index a1f50d4a5..07738d2d3 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -141,8 +141,8 @@ public function test_repository_with_nested_relations() $response = $this->getJson(CompanyRepository::uriKey().'?related=users.posts') ->assertOk(); - $this->assertCount(1, $response->json('data.0.relationships.users')); - $this->assertCount(1, $response->json('data.0.relationships.users.0.posts')); + $this->assertCount(1, $response->json('data.0.relationships')['users.posts']); + $this->assertCount(1, $response->json('data.0.relationships')['users.posts'][0]['posts']); } public function test_paginated_repository_with_relations() diff --git a/tests/Feature/Filters/FilterDefinitionTest.php b/tests/Feature/Filters/FilterDefinitionTest.php index 32653b9f1..3d2ee16ec 100644 --- a/tests/Feature/Filters/FilterDefinitionTest.php +++ b/tests/Feature/Filters/FilterDefinitionTest.php @@ -94,7 +94,8 @@ public function test_can_filter_using_belongs_to_field() ]), ]); - $json = $this->getJson(PostRepository::uriKey().'?related=user&sort=-users.name')->json(); + $json = $this->getJson(PostRepository::uriKey().'?related=user&sort=-users.name') + ->json(); $this->assertSame( 'Zez',