Skip to content

Commit

Permalink
feat: add detach relationship command
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Aug 19, 2023
1 parent 1dcc3d1 commit 9a98f71
Show file tree
Hide file tree
Showing 16 changed files with 1,472 additions and 15 deletions.
59 changes: 59 additions & 0 deletions src/Contracts/Http/Hooks/DetachRelationshipImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/*
* Copyright 2023 Cloud Creativity Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Contracts\Http\Hooks;

use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use LaravelJsonApi\Contracts\Query\QueryParameters;

interface DetachRelationshipImplementation
{
/**
* @param object $model
* @param string $fieldName
* @param Request $request
* @param QueryParameters $query
* @return void
* @throws HttpResponseException
*/
public function detachingRelationship(
object $model,
string $fieldName,
Request $request,
QueryParameters $query,
): void;

/**
* @param object $model
* @param string $fieldName
* @param mixed $related
* @param Request $request
* @param QueryParameters $query
* @return void
* @throws HttpResponseException
*/
public function detachedRelationship(
object $model,
string $fieldName,
mixed $related,
Request $request,
QueryParameters $query,
): void;
}
40 changes: 40 additions & 0 deletions src/Core/Auth/ResourceAuthorizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,46 @@ public function attachRelationshipOrFail(?Request $request, object $model, strin
}
}

/**
* Authorize a JSON:API detach relationship command.
*
* @param Request|null $request
* @param object $model
* @param string $fieldName
* @return ErrorList|null
* @throws AuthorizationException
* @throws AuthenticationException
* @throws HttpExceptionInterface
*/
public function detachRelationship(?Request $request, object $model, string $fieldName): ?ErrorList
{
$passes = $this->authorizer->attachRelationship(
$request,
$model,
$fieldName,
);

return $passes ? null : $this->failed();
}

/**
* Authorize a JSON:API detach relationship command, or fail.
*
* @param Request|null $request
* @param object $model
* @param string $fieldName
* @return void
* @throws AuthorizationException
* @throws AuthenticationException
* @throws HttpExceptionInterface
*/
public function detachRelationshipOrFail(?Request $request, object $model, string $fieldName): void
{
if ($errors = $this->attachRelationship($request, $model, $fieldName)) {
throw new JsonApiException($errors);
}
}

/**
* @return ErrorList
* @throws AuthorizationException
Expand Down
8 changes: 8 additions & 0 deletions src/Core/Bus/Commands/Command/IsRelatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

namespace LaravelJsonApi\Core\Bus\Commands\Command;

use LaravelJsonApi\Core\Extensions\Atomic\Operations\UpdateToMany;
use LaravelJsonApi\Core\Extensions\Atomic\Operations\UpdateToOne;

interface IsRelatable extends IsIdentifiable
{
/**
Expand All @@ -27,4 +30,9 @@ interface IsRelatable extends IsIdentifiable
* @return string
*/
public function fieldName(): string;

/**
* @return UpdateToOne|UpdateToMany
*/
public function operation(): UpdateToOne|UpdateToMany;
}
141 changes: 141 additions & 0 deletions src/Core/Bus/Commands/DetachRelationship/DetachRelationshipCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
/*
* Copyright 2023 Cloud Creativity Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Core\Bus\Commands\DetachRelationship;

use Illuminate\Http\Request;
use LaravelJsonApi\Contracts\Http\Hooks\DetachRelationshipImplementation;
use LaravelJsonApi\Core\Bus\Commands\Command\Command;
use LaravelJsonApi\Core\Bus\Commands\Command\HasQuery;
use LaravelJsonApi\Core\Bus\Commands\Command\Identifiable;
use LaravelJsonApi\Core\Bus\Commands\Command\IsRelatable;
use LaravelJsonApi\Core\Document\Input\Values\ResourceId;
use LaravelJsonApi\Core\Document\Input\Values\ResourceType;
use LaravelJsonApi\Core\Extensions\Atomic\Operations\UpdateToMany;
use LaravelJsonApi\Core\Support\Contracts;

class DetachRelationshipCommand extends Command implements IsRelatable
{
use Identifiable;
use HasQuery;

/**
* @var DetachRelationshipImplementation|null
*/
private ?DetachRelationshipImplementation $hooks = null;

/**
* Fluent constructor
*
* @param Request|null $request
* @param UpdateToMany $operation
* @return self
*/
public static function make(?Request $request, UpdateToMany $operation): self
{
return new self($request, $operation);
}

/**
* DetachRelationshipCommand constructor
*
* @param Request|null $request
* @param UpdateToMany $operation
*/
public function __construct(?Request $request, private readonly UpdateToMany $operation)
{
Contracts::assert(
$this->operation->isDetachingRelationship(),
'Expecting a to-many operation that is to detach resources from a relationship.',
);

parent::__construct($request);
}

/**
* @inheritDoc
* @TODO support operation with a href.
*/
public function type(): ResourceType
{
$type = $this->operation->ref()?->type;

assert($type !== null, 'Expecting an update relationship operation with a ref.');

return $type;
}

/**
* @inheritDoc
* @TODO support operation with a href
*/
public function id(): ResourceId
{
$id = $this->operation->ref()?->id;

assert($id !== null, 'Expecting an update relationship operation with a ref that has an id.');

return $id;
}

/**
* @inheritDoc
*/
public function fieldName(): string
{
$fieldName = $this->operation->ref()?->relationship ?? $this->operation->href()?->getRelationshipName();

assert(
is_string($fieldName),
'Expecting update relationship operation to have a field name.',
);

return $fieldName;
}

/**
* @inheritDoc
*/
public function operation(): UpdateToMany
{
return $this->operation;
}

/**
* Set the hooks implementation.
*
* @param DetachRelationshipImplementation|null $hooks
* @return $this
*/
public function withHooks(?DetachRelationshipImplementation $hooks): self
{
$copy = clone $this;
$copy->hooks = $hooks;

return $copy;
}

/**
* @return DetachRelationshipImplementation|null
*/
public function hooks(): ?DetachRelationshipImplementation
{
return $this->hooks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/*
* Copyright 2023 Cloud Creativity Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

declare(strict_types=1);

namespace LaravelJsonApi\Core\Bus\Commands\DetachRelationship;

use LaravelJsonApi\Contracts\Store\Store;
use LaravelJsonApi\Core\Bus\Commands\DetachRelationship\Middleware\AuthorizeDetachRelationshipCommand;
use LaravelJsonApi\Core\Bus\Commands\DetachRelationship\Middleware\TriggerDetachRelationshipHooks;
use LaravelJsonApi\Core\Bus\Commands\Middleware\SetModelIfMissing;
use LaravelJsonApi\Core\Bus\Commands\Middleware\ValidateRelationshipCommand;
use LaravelJsonApi\Core\Bus\Commands\Result;
use LaravelJsonApi\Core\Extensions\Atomic\Results\Result as Payload;
use LaravelJsonApi\Core\Support\Contracts;
use LaravelJsonApi\Core\Support\PipelineFactory;
use UnexpectedValueException;

class DetachRelationshipCommandHandler
{
/**
* DetachRelationshipCommandHandler constructor
*
* @param PipelineFactory $pipelines
* @param Store $store
*/
public function __construct(
private readonly PipelineFactory $pipelines,
private readonly Store $store,
) {
}

/**
* Execute an detach relationship command.
*
* @param DetachRelationshipCommand $command
* @return Result
*/
public function execute(DetachRelationshipCommand $command): Result
{
$pipes = [
SetModelIfMissing::class,
AuthorizeDetachRelationshipCommand::class,
ValidateRelationshipCommand::class,
TriggerDetachRelationshipHooks::class,
];

$result = $this->pipelines
->pipe($command)
->through($pipes)
->via('handle')
->then(fn (DetachRelationshipCommand $cmd): Result => $this->handle($cmd));

if ($result instanceof Result) {
return $result;
}

throw new UnexpectedValueException('Expecting pipeline to return a command result.');
}

/**
* Handle the command.
*
* @param DetachRelationshipCommand $command
* @return Result
*/
private function handle(DetachRelationshipCommand $command): Result
{
$fieldName = $command->fieldName();
$validated = $command->validated();

Contracts::assert(
array_key_exists($fieldName, $validated),
sprintf('Relation %s must have a validation rule so that it is validated.', $fieldName)
);

$input = $validated[$command->fieldName()];
$model = $command->modelOrFail();

$result = $this->store
->modifyToMany($command->type(), $model, $fieldName)
->withRequest($command->request())
->detach($input);

return Result::ok(new Payload($result, true));
}
}
Loading

0 comments on commit 9a98f71

Please sign in to comment.