Skip to content

Commit

Permalink
feat: add attach relationship command
Browse files Browse the repository at this point in the history
  • Loading branch information
lindyhopchris committed Aug 19, 2023
1 parent 16f8179 commit 1dcc3d1
Show file tree
Hide file tree
Showing 16 changed files with 1,457 additions and 12 deletions.
59 changes: 59 additions & 0 deletions src/Contracts/Http/Hooks/AttachRelationshipImplementation.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 AttachRelationshipImplementation
{
/**
* @param object $model
* @param string $fieldName
* @param Request $request
* @param QueryParameters $query
* @return void
* @throws HttpResponseException
*/
public function attachingRelationship(
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 attachedRelationship(
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 @@ -342,6 +342,46 @@ public function updateRelationshipOrFail(?Request $request, object $model, strin
}
}

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

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

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

/**
* @return ErrorList
* @throws AuthorizationException
Expand Down
141 changes: 141 additions & 0 deletions src/Core/Bus/Commands/AttachRelationship/AttachRelationshipCommand.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\AttachRelationship;

use Illuminate\Http\Request;
use LaravelJsonApi\Contracts\Http\Hooks\AttachRelationshipImplementation;
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 AttachRelationshipCommand extends Command implements IsRelatable
{
use Identifiable;
use HasQuery;

/**
* @var AttachRelationshipImplementation|null
*/
private ?AttachRelationshipImplementation $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);
}

/**
* AttachRelationshipCommand constructor
*
* @param Request|null $request
* @param UpdateToMany $operation
*/
public function __construct(?Request $request, private readonly UpdateToMany $operation)
{
Contracts::assert(
$this->operation->isAttachingRelationship(),
'Expecting a to-many operation that is to attach resources to 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 AttachRelationshipImplementation|null $hooks
* @return $this
*/
public function withHooks(?AttachRelationshipImplementation $hooks): self
{
$copy = clone $this;
$copy->hooks = $hooks;

return $copy;
}

/**
* @return AttachRelationshipImplementation|null
*/
public function hooks(): ?AttachRelationshipImplementation
{
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\AttachRelationship;

use LaravelJsonApi\Contracts\Store\Store;
use LaravelJsonApi\Core\Bus\Commands\AttachRelationship\Middleware\AuthorizeAttachRelationshipCommand;
use LaravelJsonApi\Core\Bus\Commands\AttachRelationship\Middleware\TriggerAttachRelationshipHooks;
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 AttachRelationshipCommandHandler
{
/**
* AttachRelationshipCommandHandler constructor
*
* @param PipelineFactory $pipelines
* @param Store $store
*/
public function __construct(
private readonly PipelineFactory $pipelines,
private readonly Store $store,
) {
}

/**
* Execute an attach relationship command.
*
* @param AttachRelationshipCommand $command
* @return Result
*/
public function execute(AttachRelationshipCommand $command): Result
{
$pipes = [
SetModelIfMissing::class,
AuthorizeAttachRelationshipCommand::class,
ValidateRelationshipCommand::class,
TriggerAttachRelationshipHooks::class,
];

$result = $this->pipelines
->pipe($command)
->through($pipes)
->via('handle')
->then(fn (AttachRelationshipCommand $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 AttachRelationshipCommand $command
* @return Result
*/
private function handle(AttachRelationshipCommand $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())
->attach($input);

return Result::ok(new Payload($result, true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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\AttachRelationship;

use Closure;
use LaravelJsonApi\Core\Bus\Commands\Result;

interface HandlesAttachRelationshipCommands
{
/**
* @param AttachRelationshipCommand $command
* @param Closure $next
* @return Result
*/
public function handle(AttachRelationshipCommand $command, Closure $next): Result;
}
Loading

0 comments on commit 1dcc3d1

Please sign in to comment.