<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Api;

use CloudCreativity\LaravelJsonApi\Codec\Codec;
use CloudCreativity\LaravelJsonApi\Codec\DecodingList;
use CloudCreativity\LaravelJsonApi\Codec\Encoding;
use CloudCreativity\LaravelJsonApi\Codec\EncodingList;
use CloudCreativity\LaravelJsonApi\Contracts\Client\ClientInterface;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Encoder\SerializerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Repositories\ErrorRepositoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Resolver\ResolverInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Http\Responses\Responses;
use CloudCreativity\LaravelJsonApi\Resolver\AggregateResolver;
use CloudCreativity\LaravelJsonApi\Resolver\NamespaceResolver;
use GuzzleHttp\Client;
use Neomerx\JsonApi\Contracts\Codec\CodecMatcherInterface;
use Neomerx\JsonApi\Contracts\Encoder\EncoderInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\SupportedExtensionsInterface;
use Neomerx\JsonApi\Encoder\EncoderOptions;

/**
 * Class Api
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Api
{

    /**
     * @var Factory
     */
    private $factory;

    /**
     * @var AggregateResolver
     */
    private $resolver;

    /**
     * @var string
     */
    private $name;

    /**
     * @var EncodingList
     */
    private $encodings;

    /**
     * @var DecodingList
     */
    private $decodings;

    /**
     * @var array
     * @deprecated 2.0.0
     */
    private $errors;

    /**
     * @var bool
     */
    private $useEloquent;

    /**
     * @var Url
     */
    private $url;

    /**
     * @var Jobs
     */
    private $jobs;

    /**
     * @var string|null
     */
    private $supportedExt;

    /**
     * @var ContainerInterface|null
     */
    private $container;

    /**
     * @var StoreInterface|null
     */
    private $store;

    /**
     * @var CodecMatcherInterface|null
     */
    private $codecMatcher;

    /**
     * @var ErrorRepositoryInterface|null
     * @deprecated 2.0.0
     */
    private $errorRepository;

    /**
     * @var Responses|null
     */
    private $responses;

    /**
     * @var array
     */
    private $providers;

    /**
     * Api constructor.
     *
     * @param Factory $factory
     * @param AggregateResolver $resolver
     * @param $apiName
     * @param EncodingList $encodings
     * @param DecodingList $decodings
     * @param Url $url
     * @param Jobs $jobs
     * @param bool $useEloquent
     * @param string|null $supportedExt
     * @param array $errors
     * @param array $providers
     */
    public function __construct(
        Factory $factory,
        AggregateResolver $resolver,
        $apiName,
        EncodingList $encodings,
        DecodingList $decodings,
        Url $url,
        Jobs $jobs,
        $useEloquent = true,
        $supportedExt = null,
        array $errors = [],
        array $providers = []
    ) {
        $this->factory = $factory;
        $this->resolver = $resolver;
        $this->name = $apiName;
        $this->encodings = $encodings;
        $this->decodings = $decodings;
        $this->url = $url;
        $this->jobs = $jobs;
        $this->useEloquent = $useEloquent;
        $this->supportedExt = $supportedExt;
        $this->errors = $errors;
        $this->providers = $providers;
    }

    /**
     * @return void
     */
    public function __clone()
    {
        $this->schemas = null;
        $this->store = null;
        $this->codecMatcher = null;
        $this->errorRepository = null;
    }

    /**
     * Get the resolver for the API and packages.
     *
     * @return ResolverInterface
     */
    public function getResolver()
    {
        return $this->resolver;
    }

    /**
     * Get the API's resolver.
     *
     * @return ResolverInterface
     */
    public function getDefaultResolver()
    {
        return $this->resolver->getDefaultResolver();
    }

    /**
     * @return bool
     */
    public function isByResource()
    {
        $resolver = $this->getDefaultResolver();

        return $resolver instanceof NamespaceResolver;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return bool
     */
    public function isEloquent()
    {
        return $this->useEloquent;
    }

    /**
     * @return Url
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * @return Jobs
     */
    public function getJobs()
    {
        return $this->jobs;
    }

    /**
     * @return ContainerInterface|null
     */
    public function getContainer()
    {
        if (!$this->container) {
            $this->container = $this->factory->createExtendedContainer($this->resolver);
        }

        return $this->container;
    }

    /**
     * @return StoreInterface
     */
    public function getStore()
    {
        if (!$this->store) {
            $this->store = $this->factory->createStore($this->getContainer());
        }

        return $this->store;
    }

    /**
     * @return ErrorRepositoryInterface
     * @deprecated 2.0.0
     */
    public function getErrors()
    {
        if (!$this->errorRepository) {
            $this->errorRepository = $this->factory->createErrorRepository($this->errors);
        }

        return $this->errorRepository;
    }

    /**
     * @return SupportedExtensionsInterface|null
     */
    public function getSupportedExtensions()
    {
        if (!$this->supportedExt) {
            return null;
        }

        return $this->factory->createSupportedExtensions($this->supportedExt);
    }

    /**
     * @return EncodingList
     */
    public function getEncodings(): EncodingList
    {
        return $this->encodings;
    }

    /**
     * @return DecodingList
     */
    public function getDecodings(): DecodingList
    {
        return $this->decodings;
    }

    /**
     * Get the default API codec.
     *
     * @return Codec
     */
    public function getDefaultCodec(): Codec
    {
        return $this->factory->createCodec(
            $this->getContainer(),
            $this->encodings->find(MediaTypeInterface::JSON_API_MEDIA_TYPE) ?: Encoding::jsonApi(),
            $this->decodings->find(MediaTypeInterface::JSON_API_MEDIA_TYPE)
        );
    }

    /**
     * Get the responses instance for the API.
     *
     * @return Responses
     */
    public function getResponses()
    {
        if (!$this->responses) {
            $this->responses = $this->response();
        }

        return $this->responses;
    }

    /**
     * Get the matched encoder, or a default encoder.
     *
     * @return EncoderInterface
     * @deprecated 2.0.0 use `encoder` to create an encoder.
     */
    public function getEncoder()
    {
        return $this->encoder();
    }

    /**
     * Create an encoder for the API.
     *
     * @param int|EncoderOptions|Encoding $options
     * @param int $depth
     * @return SerializerInterface
     */
    public function encoder($options = 0, $depth = 512)
    {
        if ($options instanceof Encoding) {
            $options = $options->getOptions();
        }

        if (!$options instanceof EncoderOptions) {
            $options = new EncoderOptions($options, $this->getUrl()->toString(), $depth);
        }

        return $this->factory->createEncoder($this->getContainer(), $options);
    }

    /**
     * Create a responses helper for this API.
     *
     * @return Responses
     */
    public function response()
    {
        return $this->factory->createResponseFactory($this);
    }

    /**
     * @param Client|string|array $clientHostOrOptions
     *      Guzzle client, string host or array of Guzzle options
     * @param array $options
     *      Guzzle options, only used if first argument is a string host name.
     * @return ClientInterface
     */
    public function client($clientHostOrOptions = [], array $options = [])
    {
        if (is_array($clientHostOrOptions)) {
            $options = $clientHostOrOptions;
            $options['base_uri'] = isset($options['base_uri']) ?
                $options['base_uri'] : $this->url->getBaseUri();
        }

        if (is_string($clientHostOrOptions)) {
            $options = array_replace($options, [
                'base_uri' => $this->url->withHost($clientHostOrOptions)->getBaseUri(),
            ]);
        }

        $client = ($clientHostOrOptions instanceof Client) ? $clientHostOrOptions : new Client($options);

        return $this->factory->createClient($client, $this->getContainer(), $this->encoder());
    }

    /**
     * @return UrlGenerator
     */
    public function url()
    {
        return $this->factory->createUrlGenerator($this->url);
    }

    /**
     * @return LinkGenerator
     */
    public function links()
    {
        return $this->factory->createLinkGenerator($this->url());
    }

    /**
     * @return ResourceProviders
     */
    public function providers(): ResourceProviders
    {
        return new ResourceProviders(
            $this->factory,
            $this->providers
        );
    }

    /**
     * @return ValidatorFactoryInterface
     * @deprecated 2.0.0
     */
    public function validators()
    {
        return $this->factory->createValidatorFactory(
            $this->getErrors(),
            $this->getStore()
        );
    }

    /**
     * Register a resource provider with this API.
     *
     * @param AbstractProvider $provider
     * @return void
     */
    public function register(AbstractProvider $provider)
    {
        $this->resolver->attach($provider->getResolver());
        $this->errors = array_replace($provider->getErrors(), $this->errors);
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Codec;

use CloudCreativity\LaravelJsonApi\Contracts\Decoder\DecoderInterface;
use CloudCreativity\LaravelJsonApi\Decoder\JsonApiDecoder;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Http\Headers\MediaType;

/**
 * Class Decoding
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Decoding
{

    /**
     * @var MediaTypeInterface
     */
    private $mediaType;

    /**
     * @var DecoderInterface
     */
    private $decoder;

    /**
     * Create a decoding.
     *
     * @param string|MediaTypeInterface $mediaType
     * @param string|DecoderInterface $decoder
     * @return Decoding
     */
    public static function create($mediaType, $decoder): self
    {
        if (is_string($mediaType)) {
            $mediaType = MediaType::parse(0, $mediaType);
        }

        if (!$mediaType instanceof MediaTypeInterface) {
            throw new \InvalidArgumentException('Expecting a media type object or string.');
        }

        if (is_string($decoder)) {
            $decoder = app($decoder);
        }

        if (!$decoder instanceof DecoderInterface) {
            throw new \InvalidArgumentException('Expecting a decoder or decoder service name.');
        }

        return new self($mediaType, $decoder);
    }

    /**
     * @param $key
     * @param $value
     * @return Decoding
     */
    public static function fromArray($key, $value): self
    {
        if (is_numeric($key)) {
            $key = $value;
            $value = new JsonApiDecoder();
        }

        return self::create($key, $value);
    }

    /**
     * Decoding constructor.
     *
     * @param MediaTypeInterface $mediaType
     * @param DecoderInterface $decoder
     */
    public function __construct(MediaTypeInterface $mediaType, DecoderInterface $decoder)
    {
        $this->mediaType = $mediaType;
        $this->decoder = $decoder;
    }

    /**
     * @return MediaTypeInterface
     */
    public function getMediaType(): MediaTypeInterface
    {
        return $this->mediaType;
    }

    /**
     * @return DecoderInterface
     */
    public function getDecoder(): DecoderInterface
    {
        return $this->decoder;
    }

    /**
     * @return JsonApiDecoder
     */
    public function getJsonApiDecoder(): JsonApiDecoder
    {
        if ($this->decoder instanceof JsonApiDecoder) {
            return $this->decoder;
        }

        throw new RuntimeException('Decoder is not a JSON API decoder.');
    }

    /**
     * Will the decoding decode JSON API content?
     *
     * @return bool
     */
    public function isJsonApi(): bool
    {
        return $this->decoder instanceof JsonApiDecoder;
    }

    /**
     * @return bool
     */
    public function isNotJsonApi(): bool
    {
        return $this->isJsonApi();
    }

    /**
     * @param MediaTypeInterface $mediaType
     * @return bool
     * @todo normalization will not be necessary for neomerx/json-api:^3.0
     * @see https://github.com/neomerx/json-api/issues/221
     */
    public function equalsTo(MediaTypeInterface $mediaType): bool
    {
        return $this->normalize($this->mediaType)->equalsTo(
            $this->normalize($mediaType)
        );
    }

    /**
     * @return array
     */
    private function getWildCardParameters(): array
    {
        return collect((array) $this->mediaType->getParameters())->filter(function ($value) {
            return '*' === $value;
        })->keys()->all();
    }

    /**
     * @param MediaTypeInterface $mediaType
     * @return MediaTypeInterface
     */
    private function normalize(MediaTypeInterface $mediaType): MediaTypeInterface
    {
        $params = collect((array) $mediaType->getParameters())->forget(
            $this->getWildCardParameters()
        )->all();

        return new MediaType($mediaType->getType(), $mediaType->getSubType(), $params ?: null);
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Codec;

use Neomerx\JsonApi\Contracts\Http\Headers\HeaderInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Http\Headers\MediaType;

/**
 * Class DecodingList
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class DecodingList implements \IteratorAggregate, \Countable
{

    /**
     * @var Decoding[]
     */
    private $stack;

    /**
     * @param iterable $input
     * @return DecodingList
     */
    public static function fromArray(iterable $input): self
    {
        $list = new self();
        $list->stack = collect($input)->map(function ($value, $key) {
            return Decoding::fromArray($key, $value);
        })->all();

        return $list;
    }

    /**
     * DecodingList constructor.
     *
     * @param Decoding ...$decodings
     */
    public function __construct(Decoding ...$decodings)
    {
        $this->stack = $decodings;
    }

    /**
     * Return a new instance with the supplied decodings added to the beginning of the stack.
     *
     * @param Decoding ...$decodings
     * @return DecodingList
     */
    public function prepend(Decoding ...$decodings): self
    {
        $copy = clone $this;
        array_unshift($copy->stack, ...$decodings);

        return $copy;
    }

    /**
     * Return a new instance with the supplied decodings added to the end of the stack.
     *
     * @param Decoding ...$decodings
     * @return DecodingList
     */
    public function push(Decoding ...$decodings): self
    {
        $copy = new self();
        $copy->stack = collect($this->stack)->merge($decodings)->all();

        return $copy;
    }

    /**
     * Return a new instance with the supplied decodings merged.
     *
     * @param DecodingList $decodings
     * @return DecodingList
     */
    public function merge(DecodingList $decodings): self
    {
        $copy = new self();
        $copy->stack = collect($this->stack)->merge($decodings->stack)->all();

        return $copy;
    }

    /**
     * Push decodings if the truth test evaluates to true.
     *
     * @param bool $test
     * @param Decoding|iterable|\Closure $decodings
     * @return DecodingList
     */
    public function when(bool $test, $decodings): self
    {
        if (!$test) {
            return $this;
        }

        if ($decodings instanceof \Closure) {
            return $decodings($this);
        }

        $decodings = $decodings instanceof Decoding ? [$decodings] : $decodings;

        return $this->push(...$decodings);
    }

    /**
     * Push decodings if the truth test does not evaluate to true.
     *
     * @param bool $test
     * @param $decodings
     * @return DecodingList
     */
    public function unless(bool $test, $decodings): self
    {
        return $this->when(true !== $test, $decodings);
    }

    /**
     * Find a matching decoding by media type.
     *
     * @param string $mediaType
     * @return Decoding|null
     */
    public function find(string $mediaType): ?Decoding
    {
        return $this->equalsTo(MediaType::parse(0, $mediaType));
    }

    /**
     * Get the decoding that matches the supplied media type.
     *
     * @param MediaTypeInterface $mediaType
     * @return Decoding|null
     */
    public function equalsTo(MediaTypeInterface $mediaType): ?Decoding
    {
        return collect($this->stack)->first(function (Decoding $decoding) use ($mediaType) {
            return $decoding->equalsTo($mediaType);
        });
    }

    /**
     * @param HeaderInterface $header
     * @return Decoding|null
     */
    public function forHeader(HeaderInterface $header): ?Decoding
    {
        foreach ($header->getMediaTypes() as $mediaType) {
            if ($decoding = $this->equalsTo($mediaType)) {
                return $decoding;
            }
        }

        return null;
    }

    /**
     * @return Decoding|null
     */
    public function first(): ?Decoding
    {
        return collect($this->stack)->first();
    }

    /**
     * @return Decoding[]
     */
    public function all(): array
    {
        return $this->stack;
    }

    /**
     * @inheritDoc
     */
    public function getIterator()
    {
        return new \ArrayIterator($this->stack);
    }

    /**
     * @inheritDoc
     */
    public function count()
    {
        return count($this->stack);
    }

    /**
     * @return bool
     */
    public function isEmpty(): bool
    {
        return empty($this->stack);
    }

    /**
     * @return bool
     */
    public function isNotEmpty(): bool
    {
        return !$this->isEmpty();
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Utils;

/**
 * Class Arr
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Arr
{

    /**
     * Recursively camelize all keys in the provided array.
     *
     * @param array|null $data
     * @return array
     */
    public static function camelize($data)
    {
        return collect($data)->mapWithKeys(function ($value, $key) {
            $key = Str::camelize($key);

            if (is_array($value)) {
                return [$key => static::camelize($value)];
            }

            return [$key => $value];
        })->all();
    }

    /**
     * Recursively dasherize all keys in the provided array.
     *
     * @param array|null $data
     * @return array
     */
    public static function dasherize($data)
    {
        return collect($data)->mapWithKeys(function ($value, $key) {
            $key = Str::dasherize($key);

            if (is_array($value)) {
                return [$key => static::dasherize($value)];
            }

            return [$key => $value];
        })->all();
    }

    /**
     * Recursively decamelize all keys in the provided array.
     *
     * @param array|null $data
     * @return array
     */
    public static function decamelize($data)
    {
        return collect($data)->mapWithKeys(function ($value, $key) {
            $key = Str::decamelize($key);

            if (is_array($value)) {
                return [$key => static::decamelize($value)];
            }

            return [$key => $value];
        })->all();
    }

    /**
     * Recursively underscore all keys in the provided array.
     *
     * @param array|null $data
     * @return array
     */
    public static function underscore($data)
    {
        return collect($data)->mapWithKeys(function ($value, $key) {
            $key = Str::underscore($key);

            if (is_array($value)) {
                return [$key => static::underscore($value)];
            }

            return [$key => $value];
        })->all();
    }
}


<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Api;

use Illuminate\Contracts\Routing\UrlGenerator as IlluminateUrlGenerator;
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
use Neomerx\JsonApi\Contracts\Schema\SchemaFactoryInterface;

/**
 * Class LinkGenerator
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class LinkGenerator
{

    /**
     * @var SchemaFactoryInterface
     */
    private $factory;

    /**
     * @var UrlGenerator
     */
    private $urls;

    /**
     * @var IlluminateUrlGenerator
     */
    private $generator;

    /**
     * LinkGenerator constructor.
     *
     * @param SchemaFactoryInterface $factory
     * @param UrlGenerator $urls
     * @param IlluminateUrlGenerator $generator
     */
    public function __construct(SchemaFactoryInterface $factory, UrlGenerator $urls, IlluminateUrlGenerator $generator)
    {
        $this->factory = $factory;
        $this->urls = $urls;
        $this->generator = $generator;
    }

    /**
     * Get a link to the current path, adding in supplied query params.
     *
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function current($meta = null, array $queryParams = [])
    {
        $url = $this->generator->current();

        if ($queryParams) {
            $url .= '?' . http_build_query($queryParams);
        }

        return $this->factory->createLink($url, $meta, true);
    }

    /**
     * Get a link to the index of a resource type.
     *
     * @param $resourceType
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function index($resourceType, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->index($resourceType, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to create a resource object.
     *
     * @param $resourceType
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function create($resourceType, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->create($resourceType, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to read a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function read($resourceType, $id, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->read($resourceType, $id, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to update a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function update($resourceType, $id, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->update($resourceType, $id, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to delete a resource object.
     *
     * @param $resourceType
     * @param $id
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function delete($resourceType, $id, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->delete($resourceType, $id, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to a resource object's related resource.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function relatedResource($resourceType, $id, $relationshipKey, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->relatedResource($resourceType, $id, $relationshipKey, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to read a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function readRelationship($resourceType, $id, $relationshipKey, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->readRelationship($resourceType, $id, $relationshipKey, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to replace a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function replaceRelationship($resourceType, $id, $relationshipKey, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->replaceRelationship($resourceType, $id, $relationshipKey, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to add to a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array|object|null $meta
     * @param array $queryParams
     * @return LinkInterface
     */
    public function addRelationship($resourceType, $id, $relationshipKey, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->addRelationship($resourceType, $id, $relationshipKey, $queryParams),
            $meta,
            true
        );
    }

    /**
     * Get a link to remove from a resource object's relationship.
     *
     * @param $resourceType
     * @param $id
     * @param $relationshipKey
     * @param array|object|null $meta
     * @param array $queryParams
     * @return string
     */
    public function removeRelationship($resourceType, $id, $relationshipKey, $meta = null, array $queryParams = [])
    {
        return $this->factory->createLink(
            $this->urls->removeRelationship($resourceType, $id, $relationshipKey, $queryParams),
            $meta,
            true
        );
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Codec;

use Neomerx\JsonApi\Contracts\Http\Headers\AcceptHeaderInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Http\Headers\MediaType;

/**
 * Class EncodingList
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class EncodingList implements \IteratorAggregate, \Countable
{

    /**
     * @var Encoding[]
     */
    private $stack;

    /**
     * Create encodings from array config.
     *
     * @param iterable $config
     * @param string|null $urlPrefix
     * @return EncodingList
     */
    public static function fromArray(iterable $config, string $urlPrefix = null): self
    {
        return new self(
            ...collect($config)->map(function ($value, $key) use ($urlPrefix) {
                return Encoding::fromArray($key, $value, $urlPrefix);
            })->values()
        );
    }

    /**
     * Create encodings that will not encode JSON API content.
     *
     * @param string|MediaTypeInterface ...$mediaTypes
     * @return EncodingList
     */
    public static function createCustom(...$mediaTypes): self
    {
        $encodings = new self();
        $encodings->stack = collect($mediaTypes)->map(function ($mediaType) {
            return Encoding::custom($mediaType);
        })->all();

        return $encodings;
    }

    /**
     * EncodingList constructor.
     *
     * @param Encoding ...$encodings
     */
    public function __construct(Encoding ...$encodings)
    {
        $this->stack = $encodings;
    }

    /**
     * Return a new instance with the supplied encodings added to the beginning of the stack.
     *
     * @param Encoding ...$encodings
     * @return EncodingList
     */
    public function prepend(Encoding ...$encodings): self
    {
        $copy = clone $this;
        array_unshift($copy->stack, ...$encodings);

        return $copy;
    }

    /**
     * Return a new instance with the supplied encodings added to the end of the stack.
     *
     * @param Encoding ...$encodings
     * @return EncodingList
     */
    public function push(Encoding ...$encodings): self
    {
        $copy = new self();
        $copy->stack = collect($this->stack)->merge($encodings)->all();

        return $copy;
    }

    /**
     * Return a new instance with the supplied encodings merged.
     *
     * @param EncodingList $encodings
     * @return EncodingList
     */
    public function merge(EncodingList $encodings): self
    {
        $copy = new self();
        $copy->stack = collect($this->stack)->merge($encodings->stack)->all();

        return $copy;
    }

    /**
     * Return a new instance with the supplied custom encodings added to the end of the stack.
     *
     * A custom encoding is one that does not encode to JSON API.
     *
     * @param mixed ...$mediaTypes
     * @return EncodingList
     */
    public function withCustom(...$mediaTypes): self
    {
        return $this->merge(self::createCustom(...$mediaTypes));
    }

    /**
     * Push encodings if the truth test evaluates to true.
     *
     * @param bool $test
     * @param Encoding|string|iterable|\Closure|null $encodings
     * @return EncodingList
     */
    public function when(bool $test, $encodings): self
    {
        if (!$test || is_null($encodings)) {
            return $this;
        }

        if ($encodings instanceof \Closure) {
            return $encodings($this);
        }

        if (is_string($encodings)) {
            $encodings = Encoding::custom($encodings);
        }

        $encodings = $encodings instanceof Encoding ? [$encodings] : $encodings;

        return $this->push(...$encodings);
    }

    /**
     * Push encodings if the truth test does not evaluate to true.
     *
     * @param bool $test
     * @param Encoding|string|iterable|\Closure|null $encodings
     * @return EncodingList
     */
    public function unless(bool $test, $encodings): self
    {
        return $this->when(true !== $test, $encodings);
    }

    /**
     * @param Encoding|string|null $encoding
     * @return EncodingList
     */
    public function optional($encoding): self
    {
        if (is_string($encoding)) {
            $encoding = Encoding::custom($encoding);
        }

        return $encoding ? $this->push($encoding) : $this;
    }

    /**
     * Find a matching encoding by media type.
     *
     * @param string $mediaType
     * @return Encoding|null
     */
    public function find(string $mediaType): ?Encoding
    {
        return $this->matchesTo(MediaType::parse(0, $mediaType));
    }

    /**
     * Get the encoding that matches the supplied media type.
     *
     * @param MediaTypeInterface $mediaType
     * @return Encoding|null
     */
    public function matchesTo(MediaTypeInterface $mediaType): ?Encoding
    {
        return collect($this->stack)->first(function (Encoding $encoding) use ($mediaType) {
            return $encoding->matchesTo($mediaType);
        });
    }

    /**
     * Get the acceptable encoding for the supplied Accept header.
     *
     * @param AcceptHeaderInterface $accept
     * @return Encoding|null
     */
    public function acceptable(AcceptHeaderInterface $accept): ?Encoding
    {
        foreach ($accept->getMediaTypes() as $mediaType) {
            if ($encoding = $this->matchesTo($mediaType)) {
                return $encoding;
            }
        }

        return null;
    }

    /**
     * @return Encoding|null
     */
    public function first(): ?Encoding
    {
        return collect($this->stack)->first();
    }

    /**
     * @return array
     */
    public function all(): array
    {
        return $this->stack;
    }

    /**
     * @inheritDoc
     */
    public function getIterator()
    {
        return new \ArrayIterator($this->stack);
    }

    /**
     * @inheritDoc
     */
    public function count()
    {
        return count($this->stack);
    }

    /**
     * @return bool
     */
    public function isEmpty(): bool
    {
        return empty($this->stack);
    }

    /**
     * @return bool
     */
    public function isNotEmpty(): bool
    {
        return !$this->isEmpty();
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Http\Middleware;

use Closure;
use CloudCreativity\LaravelJsonApi\Api\Api;
use CloudCreativity\LaravelJsonApi\Api\Repository;
use CloudCreativity\LaravelJsonApi\Exceptions\ResourceNotFoundException;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Request;
use Illuminate\Pagination\AbstractPaginator;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

/**
 * Class BootJsonApi
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class BootJsonApi
{

    /**
     * @var Container
     */
    private $container;

    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * Start JSON API support.
     *
     * This middleware:
     *
     * - Loads the configuration for the named API that this request is being routed to.
     * - Registers the API in the service container.
     * - Substitutes bindings on the route.
     * - Overrides the Laravel current page resolver so that it uses the JSON API page parameter.
     *
     * @param Request $request
     * @param Closure $next
     * @param string $namespace
     *      the API namespace, as per your JSON API configuration.
     * @return mixed
     */
    public function handle($request, Closure $next, string $namespace)
    {
        /** Build and register the API. */
        $api = $this->bindApi($namespace, $request->getSchemeAndHttpHost() . $request->getBaseUrl());

        /** Substitute route bindings. */
        $this->substituteBindings($api);

        /** Set up the Laravel paginator to read from JSON API request instead */
        $this->bindPageResolver();

        return $next($request);
    }

    /**
     * Build the API instance and bind it into the container.
     *
     * @param $namespace
     * @param $host
     * @return Api
     */
    protected function bindApi(string $namespace, string $host): Api
    {
        /** @var Repository $repository */
        $repository = $this->container->make(Repository::class);

        $api = $repository->createApi($namespace, $host);
        $this->container->instance(Api::class, $api);
        $this->container->alias(Api::class, 'json-api.inbound');

        return $api;
    }

    /**
     * @param Api $api
     * @throws ResourceNotFoundException
     */
    protected function substituteBindings(Api $api): void
    {
        /** @var Route $route */
        $route = $this->container->make(Route::class);
        $route->substituteBindings($api->getStore());
    }

    /**
     * Override the page resolver to read the page parameter from the JSON API request.
     *
     * @return void
     */
    protected function bindPageResolver(): void
    {
        /** Override the current page resolution */
        AbstractPaginator::currentPageResolver(function ($pageName) {
            $pagination = app(EncodingParametersInterface::class)->getPaginationParameters() ?: [];

            return $pagination[$pageName] ?? null;
        });
    }

}


<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Pagination;

use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PageInterface;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Schema\CreatesLinks;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Pagination\Paginator;
use Neomerx\JsonApi\Contracts\Document\LinkInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\SortParameterInterface;
use Neomerx\JsonApi\Contracts\Http\Query\QueryParametersParserInterface;

/**
 * Trait CreatesPages
 *
 * @package CloudCreativity\LaravelJsonApi
 */
trait CreatesPages
{

    use CreatesLinks;

    /**
     * @return string
     */
    protected function getPageKey()
    {
        $key = property_exists($this, 'pageKey') ? $this->pageKey : null;

        return $key ?: 'number';
    }

    /**
     * @return string
     */
    protected function getPerPageKey()
    {
        $key = property_exists($this, 'perPageKey') ? $this->perPageKey : null;

        return $key ?: 'size';
    }

    /**
     * @return string|null
     */
    protected function getMetaKey()
    {
        return property_exists($this, 'metaKey') ? $this->metaKey : null;
    }

    /**
     * @return bool
     */
    protected function isMetaUnderscored()
    {
        return property_exists($this, 'underscoreMeta') ? (bool) $this->underscoreMeta : false;
    }

    /**
     * @param Paginator $paginator
     * @param EncodingParametersInterface $parameters
     * @return PageInterface
     */
    protected function createPage(Paginator $paginator, EncodingParametersInterface $parameters)
    {
        $params = $this->buildParams($parameters);

        return app(Factory::class)->createPage(
            $paginator,
            $this->createFirstLink($paginator, $params),
            $this->createPreviousLink($paginator, $params),
            $this->createNextLink($paginator, $params),
            $this->createLastLink($paginator, $params),
            $this->createMeta($paginator),
            $this->getMetaKey()
        );
    }

    /**
     * @param Paginator $paginator
     * @param array $params
     * @return LinkInterface
     */
    protected function createFirstLink(Paginator $paginator, array $params)
    {
        return $this->createLink(1, $paginator->perPage(), $params);
    }

    /**
     * @param Paginator $paginator
     * @param array $params
     * @return LinkInterface|null
     */
    protected function createPreviousLink(Paginator $paginator, array $params)
    {
        $previous = $paginator->currentPage() - 1;

        return $previous ? $this->createLink($previous, $paginator->perPage(), $params) : null;
    }

    /**
     * @param Paginator $paginator
     * @param array $params
     * @return LinkInterface|null
     */
    protected function createNextLink(Paginator $paginator, array $params)
    {
        $next = $paginator->currentPage() + 1;

        if ($paginator instanceof LengthAwarePaginator && $next > $paginator->lastPage()) {
            return null;
        }

        return $this->createLink($next, $paginator->perPage(), $params);
    }

    /**
     * @param Paginator $paginator
     * @param array $params
     * @return LinkInterface|null
     */
    protected function createLastLink(Paginator $paginator, array $params)
    {
        if (!$paginator instanceof LengthAwarePaginator) {
            return null;
        }

        /** In older versions of Laravel, last page can return zero for an empty collection. */
        if (1 > $last = $paginator->lastPage()) {
            $last = 1;
        }

        return $this->createLink($last, $paginator->perPage(), $params);
    }

    /**
     * Build parameters that are to be included with pagination links.
     *
     * @param EncodingParametersInterface $parameters
     * @return array
     */
    protected function buildParams(EncodingParametersInterface $parameters)
    {
        return array_filter([
            QueryParametersParserInterface::PARAM_FILTER =>
                $parameters->getFilteringParameters(),
            QueryParametersParserInterface::PARAM_SORT =>
                $this->buildSortParams((array) $parameters->getSortParameters())
        ]);
    }

    /**
     * @param int $page
     * @param int $perPage
     * @param array $parameters
     * @param array|object|null $meta
     * @return LinkInterface
     */
    protected function createLink($page, $perPage, array $parameters = [], $meta = null)
    {
        return $this->links()->current($meta, array_merge($parameters, [
            QueryParametersParserInterface::PARAM_PAGE => [
                $this->getPageKey() => $page,
                $this->getPerPageKey() => $perPage,
            ],
        ]));
    }

    /**
     * @param Paginator $paginator
     * @return array
     */
    protected function createMeta(Paginator $paginator)
    {
        $meta = [
            $this->normalizeMetaKey('current-page') => $paginator->currentPage(),
            $this->normalizeMetaKey('per-page') => $paginator->perPage(),
            $this->normalizeMetaKey('from') => $paginator->firstItem(),
            $this->normalizeMetaKey('to') => $paginator->lastItem(),
        ];

        if ($paginator instanceof LengthAwarePaginator) {
            /** In older versions of Laravel, last page can return zero for an empty collection. */
            if (1 > $last = $paginator->lastPage()) {
                $last = 1;
            }

            $meta[$this->normalizeMetaKey('total')] = $paginator->total();
            $meta[$this->normalizeMetaKey('last-page')] = $last;
        }

        return $meta;
    }

    /**
     * @param $key
     * @return string
     */
    protected function normalizeMetaKey($key)
    {
        return $this->isMetaUnderscored() ? Str::underscore($key) : $key;
    }

    /**
     * @param SortParameterInterface[] $parameters
     * @return string|null
     */
    private function buildSortParams(array $parameters)
    {
        $sort = array_map(function (SortParameterInterface $param) {
            return (string) $param;
        }, $parameters);

        return !empty($sort) ? implode(',', $sort) : null;
    }
}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Queue;

trait ClientDispatchable
{

    /**
     * @var ClientJob|null
     */
    public $clientJob;

    /**
     * Start a client dispatch.
     *
     * @param mixed ...$args
     * @return ClientDispatch
     */
    public static function client(...$args): ClientDispatch
    {
        return new ClientDispatch(
            new static(...$args)
        );
    }

    /**
     * Was the job dispatched by a client?
     *
     * @return bool
     */
    public function wasClientDispatched(): bool
    {
        return !is_null($this->clientJob);
    }

    /**
     * Get the JSON API that the job belongs to.
     *
     * @return string|null
     */
    public function api(): ?string
    {
        return optional($this->clientJob)->api;
    }

    /**
     * Get the JSON API resource type that the job relates to.
     *
     * @return string|null
     */
    public function resourceType(): ?string
    {
        return optional($this->clientJob)->resource_type;
    }

    /**
     * Get the JSON API resource id that the job relates to.
     *
     * @return string|null
     */
    public function resourceId(): ?string
    {
        return optional($this->clientJob)->resource_id;
    }

    /**
     * Set the resource that was created by the job.
     *
     * If a job is creating a new resource, this method can be used to update
     * the client job with the created resource. This method does nothing if the
     * job was not dispatched by a client.
     *
     * @param $resource
     * @return void
     */
    public function didCreate($resource): void
    {
        if ($this->wasClientDispatched()) {
            $this->clientJob->setResource($resource)->save();
        }
    }
}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Api;

use Illuminate\Contracts\Routing\UrlRoutable;

/**
 * Class Url
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class Url
{

    /**
     * @var string
     */
    private $host;

    /**
     * @var string
     */
    private $namespace;

    /**
     * @var string
     */
    private $name;

    /**
     * Create a URL from an array.
     *
     * @param array $url
     * @return Url
     */
    public static function fromArray(array $url): self
    {
        return new self(
            isset($url['host']) ? $url['host'] : '',
            isset($url['namespace']) ? $url['namespace'] : '',
            isset($url['name']) ? $url['name'] : ''
        );
    }

    /**
     * Url constructor.
     *
     * @param string $host
     * @param string $namespace
     * @param string $name
     */
    public function __construct(string $host, string $namespace, string $name)
    {
        $this->host = rtrim($host, '/');
        $this->namespace = $namespace ? '/' . ltrim($namespace, '/') : '';
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->toString();
    }

    /**
     * @return string
     */
    public function toString(): string
    {
        return rtrim($this->host . $this->namespace, '/');
    }

    /**
     * @param $host
     * @return Url
     */
    public function withHost($host): self
    {
        $copy = clone $this;
        $copy->host = rtrim($host, '/');

        return $copy;
    }

    /**
     * @return string
     */
    public function getHost(): string
    {
        return $this->host;
    }

    /**
     * @return string
     */
    public function getNamespace(): string
    {
        return $this->namespace;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Get the base URI for a Guzzle client.
     *
     * @return string
     */
    public function getBaseUri(): string
    {
        return $this->toString() . '/';
    }

    /**
     * Get the URL for the resource type.
     *
     * @param string $type
     * @param array $params
     * @return string
     */
    public function getResourceTypeUrl(string $type, array $params = []): string
    {
        return $this->url([$type], $params);
    }

    /**
     * Get the URL for the specified resource.
     *
     * @param string $type
     * @param mixed $id
     * @param array $params
     * @return string
     */
    public function getResourceUrl(string $type, $id, array $params = []): string
    {
        return $this->url([$type, $id], $params);
    }

    /**
     * Get the URI for a related resource.
     *
     * @param string $type
     * @param mixed $id
     * @param string $field
     * @param array $params
     * @return string
     */
    public function getRelatedUrl(string $type, $id, string $field, array $params = []): string
    {
        return $this->url([$type, $id, $field], $params);
    }

    /**
     * Get the URI for the resource's relationship.
     *
     * @param string $type
     * @param mixed $id
     * @param string $field
     * @param array $params
     * @return string
     */
    public function getRelationshipUri(string $type, $id, string $field, array $params = []): string
    {
        return $this->url([$type, $id, 'relationships', $field], $params);
    }

    /**
     * @param array $extra
     * @param array $params
     * @return string
     */
    private function url(array $extra, array $params = []): string
    {
        $url = collect([$this->toString()])->merge($extra)->map(function ($value) {
            return $value instanceof UrlRoutable ? $value->getRouteKey() : (string) $value;
        })->implode('/');

        return $params ? $url . '?' . http_build_query($params) : $url;
    }

}


<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Http\Controllers;

use Closure;
use CloudCreativity\LaravelJsonApi\Auth\AuthorizesRequests;
use CloudCreativity\LaravelJsonApi\Codec\ChecksMediaTypes;
use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PageInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Http\Requests\CreateResource;
use CloudCreativity\LaravelJsonApi\Http\Requests\DeleteResource;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchProcess;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchProcesses;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchRelated;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchRelationship;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchResource;
use CloudCreativity\LaravelJsonApi\Http\Requests\FetchResources;
use CloudCreativity\LaravelJsonApi\Http\Requests\UpdateRelationship;
use CloudCreativity\LaravelJsonApi\Http\Requests\UpdateResource;
use CloudCreativity\LaravelJsonApi\Utils\InvokesHooks;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Routing\Controller;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class JsonApiController
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class JsonApiController extends Controller
{

    use CreatesResponses,
        AuthorizesRequests,
        ChecksMediaTypes,
        InvokesHooks;

    /**
     * The database connection name to use for transactions, or null for the default connection.
     *
     * @var string|null
     */
    protected $connection;

    /**
     * Whether database transactions should be used.
     *
     * @var bool
     */
    protected $useTransactions = true;

    /**
     * Index action.
     *
     * @param StoreInterface $store
     * @param FetchResources $request
     * @return Response
     */
    public function index(StoreInterface $store, FetchResources $request)
    {
        $result = $this->doSearch($store, $request);

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->content($result);
    }

    /**
     * Read resource action.
     *
     * @param StoreInterface $store
     * @param FetchResource $request
     * @return Response
     */
    public function read(StoreInterface $store, FetchResource $request)
    {
        $result = $this->doRead($store, $request);

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->content($result);
    }

    /**
     * Create resource action.
     *
     * @param StoreInterface $store
     * @param CreateResource $request
     * @return Response
     */
    public function create(StoreInterface $store, CreateResource $request)
    {
        $record = $this->transaction(function () use ($store, $request) {
            return $this->doCreate($store, $request);
        });

        if ($this->isResponse($record)) {
            return $record;
        }

        return $this->reply()->created($record);
    }

    /**
     * Update resource action.
     *
     * @param StoreInterface $store
     * @param UpdateResource $request
     * @return Response
     */
    public function update(StoreInterface $store, UpdateResource $request)
    {
        $record = $this->transaction(function () use ($store, $request) {
            return $this->doUpdate($store, $request);
        });

        if ($this->isResponse($record)) {
            return $record;
        }

        return $this->reply()->updated($record);
    }

    /**
     * Delete resource action.
     *
     * @param StoreInterface $store
     * @param DeleteResource $request
     * @return Response
     */
    public function delete(StoreInterface $store, DeleteResource $request)
    {
        $result = $this->transaction(function () use ($store, $request) {
            return $this->doDelete($store, $request);
        });

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->deleted($result);
    }

    /**
     * Read related resource action.
     *
     * @param StoreInterface $store
     * @param FetchRelated $request
     * @return Response
     */
    public function readRelatedResource(StoreInterface $store, FetchRelated $request)
    {
        $record = $request->getRecord();
        $result = $this->beforeReadingRelationship($record, $request);

        if ($this->isResponse($result)) {
            return $result;
        }

        $related = $store->queryRelated(
            $record,
            $request->getRelationshipName(),
            $request->getParameters()
        );

        $records = ($related instanceof PageInterface) ? $related->getData() : $related;
        $result = $this->afterReadingRelationship($record, $records, $request);

        if ($this->isInvokedResult($result)) {
            return $result;
        }

        return $this->reply()->content($related);
    }

    /**
     * Read relationship data action.
     *
     * @param StoreInterface $store
     * @param FetchRelationship $request
     * @return Response
     */
    public function readRelationship(StoreInterface $store, FetchRelationship $request)
    {
        $record = $request->getRecord();
        $result = $this->beforeReadingRelationship($record, $request);

        if ($this->isResponse($result)) {
            return $result;
        }

        $related = $store->queryRelationship(
            $record,
            $request->getRelationshipName(),
            $request->getParameters()
        );

        $records = ($related instanceof PageInterface) ? $related->getData() : $related;
        $result = $this->afterReadingRelationship($record, $records, $request);

        if ($this->isInvokedResult($result)) {
            return $result;
        }

        return $this->reply()->relationship($related);
    }

    /**
     * Replace relationship data action.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return Response
     */
    public function replaceRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $result = $this->transaction(function () use ($store, $request) {
            return $this->doReplaceRelationship($store, $request);
        });

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->noContent();
    }

    /**
     * Add to relationship data action.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return Response
     */
    public function addToRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $result = $this->transaction(function () use ($store, $request) {
            return $this->doAddToRelationship($store, $request);
        });

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->noContent();
    }

    /**
     * Remove from relationship data action.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return Response
     */
    public function removeFromRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $result = $this->transaction(function () use ($store, $request) {
            return $this->doRemoveFromRelationship($store, $request);
        });

        if ($this->isResponse($result)) {
            return $result;
        }

        return $this->reply()->noContent();
    }

    /**
     * Read processes action.
     *
     * @param StoreInterface $store
     * @param FetchProcesses $request
     * @return Response
     */
    public function processes(StoreInterface $store, FetchProcesses $request)
    {
        $result = $store->queryRecords(
            $request->getProcessType(),
            $request->getEncodingParameters()
        );

        return $this->reply()->content($result);
    }

    /**
     * Read a process action.
     *
     * @param StoreInterface $store
     * @param FetchProcess $request
     * @return Response
     */
    public function process(StoreInterface $store, FetchProcess $request)
    {
        $record = $store->readRecord(
            $request->getProcess(),
            $request->getEncodingParameters()
        );

        return $this->reply()->process($record);
    }

    /**
     * Search resources.
     *
     * @param StoreInterface $store
     * @param FetchResources $request
     * @return mixed
     */
    protected function doSearch(StoreInterface $store, FetchResources $request)
    {
        if ($result = $this->invoke('searching', $request)) {
            return $result;
        }

        $found = $store->queryRecords($request->getResourceType(), $request->getParameters());
        $records = ($found instanceof PageInterface) ? $found->getData() : $found;

        if ($result = $this->invoke('searched', $records, $request)) {
            return $result;
        }

        return $found;
    }

    /**
     * Read a resource.
     *
     * @param StoreInterface $store
     * @param FetchResource $request
     * @return mixed
     */
    protected function doRead(StoreInterface $store, FetchResource $request)
    {
        $record = $request->getRecord();

        if ($result = $this->invoke('reading', $record, $request)) {
            return $result;
        }

        /** We pass to the store for filtering, eager loading etc. */
        $record = $store->readRecord($record, $request->getParameters());

        if ($result = $this->invoke('didRead', $record, $request)) {
            return $result;
        }

        return $record;
    }

    /**
     * Create a resource.
     *
     * @param StoreInterface $store
     * @param CreateResource $request
     * @return mixed
     *      the created record, an asynchronous process, or a HTTP response.
     */
    protected function doCreate(StoreInterface $store, CreateResource $request)
    {
        if ($response = $this->beforeCommit($request)) {
            return $response;
        }

        $record = $store->createRecord(
            $request->getResourceType(),
            $request->all(),
            $request->getParameters()
        );

        return $this->afterCommit($request, $record, false) ?: $record;
    }

    /**
     * Update a resource.
     *
     * @param StoreInterface $store
     * @param UpdateResource $request
     * @return mixed
     *      the updated record, an asynchronous process, or a HTTP response.
     */
    protected function doUpdate(StoreInterface $store, UpdateResource $request)
    {
        if ($response = $this->beforeCommit($request)) {
            return $response;
        }

        $record = $store->updateRecord(
            $request->getRecord(),
            $request->all(),
            $request->getParameters()
        );

        return $this->afterCommit($request, $record, true) ?: $record;
    }

    /**
     * Delete a resource.
     *
     * @param StoreInterface $store
     * @param DeleteResource $request
     * @return mixed|null
     *      an HTTP response, an asynchronous process, content to return, or null.
     */
    protected function doDelete(StoreInterface $store, DeleteResource $request)
    {
        $record = $request->getRecord();

        if ($response = $this->invoke('deleting', $record, $request)) {
            return $response;
        }

        $result = $store->deleteRecord($record, $request->getParameters());

        return $this->invoke('deleted', $record, $request) ?: $result;
    }

    /**
     * Replace a relationship.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return mixed
     */
    protected function doReplaceRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $record = $request->getRecord();
        $name = Str::classify($field = $request->getRelationshipName());

        if ($result = $this->invokeMany(['replacing', "replacing{$name}"], $record, $request)) {
            return $result;
        }

        $record = $store->replaceRelationship(
            $record,
            $field,
            $request->all(),
            $request->getParameters()
        );

        return $this->invokeMany(["replaced{$name}", "replaced"], $record, $request) ?: $record;
    }

    /**
     * Add to a relationship.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return mixed
     */
    protected function doAddToRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $record = $request->getRecord();
        $name = Str::classify($field = $request->getRelationshipName());

        if ($result = $this->invokeMany(['adding', "adding{$name}"], $record, $request)) {
            return $result;
        }

        $record = $store->addToRelationship(
            $record,
            $field,
            $request->all(),
            $request->getParameters()
        );

        return $this->invokeMany(["added{$name}", "added"], $record, $request) ?: $record;
    }

    /**
     * Remove from a relationship.
     *
     * @param StoreInterface $store
     * @param UpdateRelationship $request
     * @return mixed
     */
    protected function doRemoveFromRelationship(StoreInterface $store, UpdateRelationship $request)
    {
        $record = $request->getRecord();
        $name = Str::classify($field = $request->getRelationshipName());

        if ($result = $this->invokeMany(['removing', "removing{$name}"], $record, $request)) {
            return $result;
        }

        $record = $store->removeFromRelationship(
            $record,
            $field,
            $request->all(),
            $request->getParameters()
        );

        return $this->invokeMany(["removed{$name}", "removed"], $record, $request) ?: $record;
    }

    /**
     * Execute the closure within an optional transaction.
     *
     * @param Closure $closure
     * @return mixed
     */
    protected function transaction(Closure $closure)
    {
        if (!$this->useTransactions) {
            return $closure();
        }

        return app('db')->connection($this->connection)->transaction($closure);
    }

    /**
     * Can the controller return the provided value?
     *
     * @param $value
     * @return bool
     */
    protected function isResponse($value)
    {
        return $value instanceof Response || $value instanceof Responsable;
    }

    /**
     * @param CreateResource|UpdateResource $request
     * @return mixed|null
     */
    private function beforeCommit($request)
    {
        $record = ($request instanceof UpdateResource) ? $request->getRecord() : null;

        if ($result = $this->invoke('saving', $record, $request)) {
            return $result;
        }

        return is_null($record) ?
            $this->invoke('creating', $request) :
            $this->invoke('updating', $record, $request);
    }

    /**
     * @param CreateResource|UpdateResource $request
     * @param $record
     * @param $updating
     * @return mixed|null
     */
    private function afterCommit($request, $record, $updating)
    {
        $method = !$updating ? 'created' : 'updated';

        if ($result = $this->invoke($method, $record, $request)) {
            return $result;
        }

        return $this->invoke('saved', $record, $request);
    }

    /**
     * @param $record
     * @param FetchRelated|FetchRelationship $request
     * @return mixed|null
     */
    private function beforeReadingRelationship($record, $request)
    {
        $field = Str::classify($request->getRelationshipName());
        $hooks = ['readingRelationship', "reading{$field}"];

        return $this->invokeMany($hooks, $record, $request);
    }

    /**
     * @param $record
     * @param $related
     *      the related resources that will be in the response.
     * @param FetchRelated|FetchRelationship $request
     * @return mixed|null
     */
    private function afterReadingRelationship($record, $related, $request)
    {
        $field = Str::classify($request->getRelationshipName());
        $hooks = ["didRead{$field}", 'didReadRelationship'];

        return $this->invokeMany($hooks, $record, $related, $request);
    }

    /**
     * @param $value
     * @return bool
     */
    protected function isInvokedResult($value): bool
    {
        return $value instanceof AsynchronousProcess || $this->isResponse($value);
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Routing;

use Illuminate\Contracts\Routing\Registrar;

/**
 * Class RouteRegistrar
 *
 * @package CloudCreativity\LaravelJsonApi
 * @method \Illuminate\Routing\Route get(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route post(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route put(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route delete(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route patch(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route options(string $uri, \Closure|array|string|null $action = null)
 * @method \Illuminate\Routing\Route any(string $uri, \Closure|array|string|null $action = null)
 * @method RouteRegistration as(string $value)
 * @method RouteRegistration domain(string $value)
 * @method RouteRegistration middleware(array|string|null $middleware)
 * @method RouteRegistration name(string $value)
 * @method RouteRegistration namespace(string $value)
 * @method RouteRegistration prefix(string  $prefix)
 * @method RouteRegistration where(array  $where)
 * @method RouteRegistration controller(string $controller)
 * @method RouteRegistration field(string $field, string $inverse = null)
 */
class RouteRegistrar
{

    /**
     * @var Registrar
     */
    private $router;

    /**
     * @var array
     */
    private $options;

    /**
     * @var array
     */
    private $defaults;

    /**
     * RouteRegistrar constructor.
     *
     * @param Registrar $router
     * @param array $options
     * @param array $defaults
     */
    public function __construct(Registrar $router, array $options = [], array $defaults = [])
    {
        $this->router = $router;
        $this->options = $options;
        $this->defaults = $defaults;
    }

    /**
     * @param $name
     * @param $arguments
     * @return mixed
     */
    public function __call($name, $arguments)
    {
        return call_user_func_array([$this->route(), $name], $arguments);
    }

    /**
     * Register a custom route.

     * @return RouteRegistration
     */
    public function route(): RouteRegistration
    {
        $route = new RouteRegistration($this->router, $this, $this->defaults);
        $route->controller($this->options['controller'] ?? '');

        return $route;
    }

    /**
     * Register routes for the supplied resource type
     *
     * @param string $resourceType
     * @param array $options
     * @return ResourceRegistration
     */
    public function resource(string $resourceType, array $options = []): ResourceRegistration
    {
        return new ResourceRegistration(
            $this->router,
            $resourceType,
            array_merge($this->options, $options)
        );
    }

}


<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Exceptions;

use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
use CloudCreativity\LaravelJsonApi\Utils\Helpers;
use Exception;
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
use Neomerx\JsonApi\Exceptions\ErrorCollection;
use Neomerx\JsonApi\Exceptions\JsonApiException;

/**
 * Class ValidationException
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class ValidationException extends JsonApiException
{

    /**
     * @var ValidatorInterface|null
     */
    private $validator;

    /**
     * Create a validation exception from a validator.
     *
     * @param ValidatorInterface $validator
     * @return ValidationException
     */
    public static function create(ValidatorInterface $validator): self
    {
        $ex = new self($validator->getErrors());
        $ex->validator = $validator;

        return $ex;
    }

    /**
     * ValidationException constructor.
     *
     * @param ErrorInterface|ErrorInterface[]|ErrorCollection $errors
     * @param string|int|null $defaultHttpCode
     * @param Exception|null $previous
     */
    public function __construct($errors, $defaultHttpCode = self::DEFAULT_HTTP_CODE, Exception $previous = null)
    {
        parent::__construct(
            $errors,
            Helpers::httpErrorStatus($errors, $defaultHttpCode),
            $previous
        );
    }

    /**
     * @return ValidatorInterface|null
     */
    public function getValidator(): ?ValidatorInterface
    {
        return $this->validator;
    }
}


<?php

/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Adapter;

use CloudCreativity\LaravelJsonApi\Codec\ChecksMediaTypes;
use CloudCreativity\LaravelJsonApi\Contracts\Adapter\RelationshipAdapterInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Adapter\ResourceAdapterInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreAwareInterface;
use CloudCreativity\LaravelJsonApi\Document\ResourceObject;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use CloudCreativity\LaravelJsonApi\Store\StoreAwareTrait;
use CloudCreativity\LaravelJsonApi\Utils\InvokesHooks;
use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;

/**
 * Class AbstractResourceAdaptor
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractResourceAdapter implements ResourceAdapterInterface, StoreAwareInterface
{

    use StoreAwareTrait,
        InvokesHooks,
        ChecksMediaTypes,
        Concerns\GuardsFields,
        Concerns\FindsManyResources;

    /**
     * Create a new record.
     *
     * Implementing classes need only implement the logic to transfer the minimum
     * amount of data from the resource that is required to construct a new record
     * instance. The adapter will then fill the object after it has been
     * created.
     *
     * @param ResourceObject $resource
     * @return mixed
     *      the new domain record.
     */
    abstract protected function createRecord(ResourceObject $resource);

    /**
     * @param $record
     * @param Collection $attributes
     * @return void
     */
    abstract protected function fillAttributes($record, Collection $attributes);

    /**
     * Persist changes to the record.
     *
     * @param $record
     * @return AsynchronousProcess|null
     */
    abstract protected function persist($record);

    /**
     * Delete a record from storage.
     *
     * @param $record
     * @return bool
     *      whether the record was successfully destroyed.
     */
    abstract protected function destroy($record);

    /**
     * @inheritdoc
     */
    public function create(array $document, EncodingParametersInterface $parameters)
    {
        $record = $this->createRecord(
            $resource = $this->deserialize($document)
        );

        return $this->fillAndPersist($record, $resource, $parameters, false);
    }

    /**
     * @inheritDoc
     */
    public function read($record, EncodingParametersInterface $parameters)
    {
        return $record;
    }

    /**
     * @inheritdoc
     */
    public function update($record, array $document, EncodingParametersInterface $parameters)
    {
        $resource = $this->deserialize($document, $record);

        return $this->fillAndPersist($record, $resource, $parameters, true) ?: $record;
    }

    /**
     * @inheritDoc
     */
    public function delete($record, EncodingParametersInterface $params)
    {
        if ($result = $this->invoke('deleting', $record)) {
            return $result;
        }

        if (true !== $this->destroy($record)) {
            return false;
        }

        if ($result = $this->invoke('deleted', $record)) {
            return $result;
        }

        return true;
    }

    /**
     * @inheritDoc
     */
    public function getRelated($field)
    {
        if (!$method = $this->methodForRelation($field)) {
            throw new RuntimeException("No relationship method implemented for field {$field}.");
        }

        $relation = $this->{$method}();

        if (!$relation instanceof RelationshipAdapterInterface) {
            throw new RuntimeException("Method {$method} did not return a relationship adapter.");
        }

        $relation->withFieldName($field);

        if ($relation instanceof StoreAwareInterface) {
            $relation->withStore($this->getStore());
        }

        return $relation;
    }

    /**
     * Deserialize a resource object from a JSON API document.
     *
     * @param array $document
     * @param mixed|null $record
     * @return ResourceObject
     */
    protected function deserialize(array $document, $record = null): ResourceObject
    {
        $data = $document['data'] ?? [];

        if (!is_array($data) || empty($data)) {
            throw new \InvalidArgumentException('Expecting a JSON API document with a data member.');
        }

        return ResourceObject::create($data);
    }

    /**
     * @param $field
     * @return bool
     */
    protected function isRelation($field)
    {
        return !empty($this->methodForRelation($field));
    }

    /**
     * Is the field a fillable relation?
     *
     * @param $field
     * @param $record
     * @return bool
     */
    protected function isFillableRelation($field, $record)
    {
        return $this->isRelation($field) && $this->isFillable($field, $record);
    }

    /**
     * Get the method name on this adapter for the supplied JSON API field.
     *
     * By default we expect the developer to be following the PSR1 standard,
     * so the method name on the adapter should use camel case.
     *
     * However, some developers may prefer to use the actual JSON API field
     * name. E.g. they could use `user_history` as the JSON API field name
     * and the method name.
     *
     * Therefore we return the field name if it exactly exists on the adapter,
     * otherwise we camelize it.
     *
     * A developer can use completely different logic by overloading this
     * method.
     *
     * @param string $field
     *      the JSON API field name.
     * @return string|null
     *      the adapter's method name, or null if none is implemented.
     */
    protected function methodForRelation($field)
    {
        if (method_exists($this, $field)) {
            return $field;
        }

        $method = Str::camelize($field);

        return method_exists($this, $method) ? $method : null;
    }

    /**
     * Fill the domain record with data from the supplied resource object.
     *
     * @param $record
     * @param ResourceObject $resource
     * @param EncodingParametersInterface $parameters
     * @return void
     */
    protected function fill($record, ResourceObject $resource, EncodingParametersInterface $parameters)
    {
        $this->fillAttributes($record, $resource->getAttributes());
        $this->fillRelationships($record, $resource->getRelationships(), $parameters);
    }

    /**
     * Fill relationships from a resource object.
     *
     * @param $record
     * @param Collection $relationships
     * @param EncodingParametersInterface $parameters
     * @return void
     */
    protected function fillRelationships(
        $record,
        Collection $relationships,
        EncodingParametersInterface $parameters
    ) {
        $relationships->filter(function ($value, $field) use ($record) {
            return $this->isFillableRelation($field, $record);
        })->each(function ($value, $field) use ($record, $parameters) {
            $this->fillRelationship($record, $field, $value, $parameters);
        });
    }

    /**
     * Fill a relationship from a resource object.
     *
     * @param $record
     * @param $field
     * @param array $relationship
     * @param EncodingParametersInterface $parameters
     */
    protected function fillRelationship(
        $record,
        $field,
        array $relationship,
        EncodingParametersInterface $parameters
    ) {
        $relation = $this->getRelated($field);

        $relation->update($record, $relationship, $parameters);
    }

    /**
     * Fill any related records that need to be filled after the primary record has been persisted.
     *
     * E.g. this is useful for hydrating many-to-many Eloquent relations, where `$record` must
     * be persisted before the many-to-many database link can be created.
     *
     * @param $record
     * @param ResourceObject $resource
     * @param EncodingParametersInterface $parameters
     */
    protected function fillRelated($record, ResourceObject $resource, EncodingParametersInterface $parameters)
    {
        // no-op
    }

    /**
     * @param mixed $record
     * @param ResourceObject $resource
     * @param EncodingParametersInterface $parameters
     * @param bool $updating
     * @return AsynchronousProcess|mixed
     */
    protected function fillAndPersist(
        $record,
        ResourceObject $resource,
        EncodingParametersInterface $parameters,
        $updating
    ) {
        $this->fill($record, $resource, $parameters);

        if ($result = $this->beforePersist($record, $resource, $updating)) {
            return $result;
        }

        $async = $this->persist($record);

        if ($async instanceof AsynchronousProcess) {
            return $async;
        }

        $this->fillRelated($record, $resource, $parameters);

        if ($result = $this->afterPersist($record, $resource, $updating)) {
            return $result;
        }

        return $record;
    }

    /**
     * @inheritDoc
     */
    protected function isInvokedResult($result): bool
    {
        return $result instanceof AsynchronousProcess;
    }

    /**
     * @param $record
     * @param ResourceObject $resource
     * @param $updating
     * @return AsynchronousProcess|null
     */
    private function beforePersist($record, ResourceObject $resource, $updating)
    {
        return $this->invokeMany([
            'saving',
            $updating ? 'updating' : 'creating',
        ], $record, $resource);
    }

    /**
     * @param $record
     * @param ResourceObject $resource
     * @param $updating
     * @return AsynchronousProcess|null
     */
    private function afterPersist($record, ResourceObject $resource, $updating)
    {
        return $this->invokeMany([
            $updating ? 'updated' : 'created',
            'saved',
        ], $record, $resource);
    }

}


<?php
/**
 * Copyright 2019 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.
 */

namespace CloudCreativity\LaravelJsonApi\Queue;

use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Routing\ResourceRegistrar;
use DateTimeInterface;
use Illuminate\Foundation\Bus\PendingDispatch;

class ClientDispatch extends PendingDispatch
{

    /**
     * @var string
     */
    protected $api;

    /**
     * @var string
     */
    protected $resourceType;

    /**
     * @var string|bool|null
     */
    protected $resourceId;

    /**
     * ClientDispatch constructor.
     *
     * @param mixed $job
     */
    public function __construct($job)
    {
        parent::__construct($job);
        $this->resourceId = false;
    }

    /**
     * @return string
     */
    public function getApi(): string
    {
        if (is_string($this->api)) {
            return $this->api;
        }

        return $this->api = json_api()->getName();
    }

    /**
     * Set the API that the job belongs to.
     *
     * @param string $api
     * @return ClientDispatch
     */
    public function setApi(string $api): ClientDispatch
    {
        $this->api = $api;

        return $this;
    }

    /**
     * Set the resource type and id that will be created/updated by the job.
     *
     * @param string $type
     * @param string|null $id
     * @return ClientDispatch
     */
    public function setResource(string $type, string $id = null): ClientDispatch
    {
        $this->resourceType = $type;
        $this->resourceId = $id;

        return $this;
    }

    /**
     * @return string
     */
    public function getResourceType(): string
    {
        if (is_string($this->resourceType)) {
            return $this->resourceType;
        }

        return $this->resourceType = request()->route(ResourceRegistrar::PARAM_RESOURCE_TYPE);
    }

    /**
     * @return string|null
     */
    public function getResourceId(): ?string
    {
        if (false !== $this->resourceId) {
            return $this->resourceId;
        }

        $request = request();
        $id = $request->route(ResourceRegistrar::PARAM_RESOURCE_ID);

        /** If the binding has been substituted, we need to re-lookup the resource id. */
        if (is_object($id)) {
            $id = json_api()->getContainer()->getSchema($id)->getId($id);
        }

        return $this->resourceId = $id ?: $request->json('data.id');
    }

    /**
     * @return DateTimeInterface|null
     */
    public function getTimeoutAt(): ?DateTimeInterface
    {
        if (method_exists($this->job, 'retryUntil')) {
            return $this->job->retryUntil();
        }

        return $this->job->retryUntil ?? null;
    }

    /**
     * @return int|null
     */
    public function getTimeout(): ?int
    {
        return $this->job->timeout ?? null;
    }

    /**
     * @return int|null
     */
    public function getMaxTries(): ?int
    {
        return $this->job->tries ?? null;
    }

    /**
     * @return AsynchronousProcess
     */
    public function dispatch(): AsynchronousProcess
    {
        $fqn = json_api($this->getApi())
            ->getJobs()
            ->getModel();

        $this->job->clientJob = new $fqn;
        $this->job->clientJob->dispatching($this);

        parent::__destruct();

        return $this->job->clientJob;
    }

    /**
     * @inheritdoc
     */
    public function __destruct()
    {
        // no-op
    }
}