<?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\Eloquent;

use CloudCreativity\LaravelJsonApi\Adapter\AbstractResourceAdapter;
use CloudCreativity\LaravelJsonApi\Contracts\Adapter\HasManyAdapterInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Adapter\RelationshipAdapterInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PageInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Pagination\PagingStrategyInterface;
use CloudCreativity\LaravelJsonApi\Document\ResourceObject;
use CloudCreativity\LaravelJsonApi\Exceptions\RuntimeException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations;
use Illuminate\Support\Collection;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;

/**
 * Class AbstractAdapter
 *
 * @package CloudCreativity\LaravelJsonApi
 */
abstract class AbstractAdapter extends AbstractResourceAdapter
{

    use Concerns\DeserializesAttributes,
        Concerns\IncludesModels,
        Concerns\SortsModels;

    /**
     * @var Model
     */
    protected $model;

    /**
     * @var PagingStrategyInterface|null
     */
    protected $paging;

    /**
     * The model key that is the primary key for the resource id.
     *
     * If empty, defaults to `Model::getRouteKeyName()`.
     *
     * @var string|null
     */
    protected $primaryKey;

    /**
     * The filter param for a find-many request.
     *
     * If null, defaults to the JSON API keyword `id`.
     *
     * @var string|null
     */
    protected $findManyFilter = null;

    /**
     * The default pagination to use if no page parameters have been provided.
     *
     * If your resource must always be paginated, use this to return the default
     * pagination variables... e.g. `['number' => 1]` for page 1.
     *
     * If this property is null or an empty array, then no pagination will be
     * used if no page parameters have been provided (i.e. every resource
     * will be returned).
     *
     * @var array|null
     */
    protected $defaultPagination = null;

    /**
     * Apply the supplied filters to the builder instance.
     *
     * @param Builder $query
     * @param Collection $filters
     * @return void
     */
    abstract protected function filter($query, Collection $filters);

    /**
     * AbstractAdapter constructor.
     *
     * @param Model $model
     * @param PagingStrategyInterface|null $paging
     */
    public function __construct(Model $model, PagingStrategyInterface $paging = null)
    {
        $this->model = $model;
        $this->paging = $paging;
    }

    /**
     * @inheritDoc
     */
    public function query(EncodingParametersInterface $parameters)
    {
        $parameters = $this->getQueryParameters($parameters);

        return $this->queryAllOrOne($this->newQuery(), $parameters);
    }

    /**
     * Query the resource when it appears in a to-many relation of a parent resource.
     *
     * For example, a request to `/posts/1/comments` will invoke this method on the
     * comments adapter.
     *
     * @param Relations\BelongsToMany|Relations\HasMany|Relations\HasManyThrough|Builder $relation
     * @param EncodingParametersInterface $parameters
     * @return mixed
     * @todo default pagination causes a problem with polymorphic relations??
     */
    public function queryToMany($relation, EncodingParametersInterface $parameters)
    {
        return $this->queryAllOrOne(
            $relation->newQuery(),
            $this->getQueryParameters($parameters)
        );
    }

    /**
     * Query the resource when it appears in a to-one relation of a parent resource.
     *
     * For example, a request to `/posts/1/author` will invoke this method on the
     * user adapter when the author relation returns a `users` resource.
     *
     * @param Relations\BelongsTo|Relations\HasOne|Builder $relation
     * @param EncodingParametersInterface $parameters
     * @return mixed
     */
    public function queryToOne($relation, EncodingParametersInterface $parameters)
    {
        return $this->queryOne(
            $relation->newQuery(),
            $this->getQueryParameters($parameters)
        );
    }

    /**
     * @inheritDoc
     */
    public function read($record, EncodingParametersInterface $parameters)
    {
        $parameters = $this->getQueryParameters($parameters);

        if (!empty($parameters->getFilteringParameters())) {
            $record = $this->readWithFilters($record, $parameters);
        }

        if ($record) {
            $this->load($record, $parameters);
        }

        return $record;
    }

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

        /** @var Model $record */
        $record = parent::update($record, $document, $parameters);
        $this->load($record, $parameters);

        return $record;
    }

    /**
     * @inheritDoc
     */
    public function exists($resourceId)
    {
        return $this->findQuery($resourceId)->exists();
    }

    /**
     * @inheritDoc
     */
    public function find($resourceId)
    {
        return $this->findQuery($resourceId)->first();
    }

    /**
     * @inheritDoc
     */
    public function findMany(array $resourceIds)
    {
        return $this->findManyQuery($resourceIds)->get()->all();
    }

    /**
     * Get a new query builder.
     *
     * Child classes can overload this method if they want to modify the query instance that
     * is used for every query the adapter does.
     *
     * @return Builder
     */
    protected function newQuery()
    {
        return $this->model->newQuery();
    }

    /**
     * @param $resourceId
     * @return Builder
     */
    protected function findQuery($resourceId)
    {
        return $this->newQuery()->where(
            $this->getQualifiedKeyName(),
            $resourceId
        );
    }

    /**
     * @param array $resourceIds
     * @return Builder
     */
    protected function findManyQuery(array $resourceIds)
    {
        return $this->newQuery()->whereIn(
            $this->getQualifiedKeyName(),
            $resourceIds
        );
    }

    /**
     * Does the record match the supplied filters?
     *
     * @param Model $record
     * @param EncodingParametersInterface $parameters
     * @return Model|null
     */
    protected function readWithFilters($record, EncodingParametersInterface $parameters)
    {
        $query = $this->newQuery()->whereKey($record->getKey());
        $this->applyFilters($query, collect($parameters->getFilteringParameters()));

        return $query->exists() ? $record : null;
    }

    /**
     * Apply filters to the provided query parameter.
     *
     * @param Builder $query
     * @param Collection $filters
     */
    protected function applyFilters($query, Collection $filters)
    {
        /** By default we support the `id` filter. */
        if ($this->isFindMany($filters)) {
            $this->filterByIds($query, $filters);
        }

        /** Hook for custom filters. */
        $this->filter($query, $filters);
    }

    /**
     * @inheritDoc
     */
    protected function createRecord(ResourceObject $resource)
    {
        return $this->model->newInstance();
    }

    /**
     * @inheritDoc
     */
    protected function destroy($record)
    {
        /** @var Model $record */
        return (bool) $record->delete();
    }

    /**
     * @inheritdoc
     */
    protected function fillRelationship(
        $record,
        $field,
        array $relationship,
        EncodingParametersInterface $parameters
    ) {
        $relation = $this->getRelated($field);

        if (!$this->requiresPrimaryRecordPersistence($relation)) {
            $relation->update($record, $relationship, $parameters);
        }
    }

    /**
     * Hydrate related models after the primary record has been persisted.
     *
     * @param Model $record
     * @param ResourceObject $resource
     * @param EncodingParametersInterface $parameters
     */
    protected function fillRelated(
        $record,
        ResourceObject $resource,
        EncodingParametersInterface $parameters
    ) {
        $relationships = $resource->getRelationships();
        $changed = false;

        foreach ($relationships as $field => $value) {
            /** Skip any fields that are not fillable. */
            if ($this->isNotFillable($field, $record)) {
                continue;
            }

            /** Skip any fields that are not relations */
            if (!$this->isRelation($field)) {
                continue;
            }

            $relation = $this->getRelated($field);

            if ($this->requiresPrimaryRecordPersistence($relation)) {
                $relation->update($record, $value, $parameters);
                $changed = true;
            }
        }

        /** If there are changes, we need to refresh the model in-case the relationship has been cached. */
        if ($changed) {
            $record->refresh();
        }
    }

    /**
     * Does the relationship need to be hydrated after the primary record has been persisted?
     *
     * @param RelationshipAdapterInterface $relation
     * @return bool
     */
    protected function requiresPrimaryRecordPersistence(RelationshipAdapterInterface $relation)
    {
        return $relation instanceof HasManyAdapterInterface || $relation instanceof HasOne;
    }

    /**
     * @inheritdoc
     */
    protected function persist($record)
    {
        $record->save();
    }

    /**
     * @param $query
     * @param Collection $filters
     * @return void
     */
    protected function filterByIds($query, Collection $filters)
    {
        $query->whereIn(
            $this->getQualifiedKeyName(),
            $this->extractIds($filters)
        );
    }

    /**
     * Return the result for a search one query.
     *
     * @param Builder $query
     * @return Model
     */
    protected function searchOne($query)
    {
        return $query->first();
    }

    /**
     * Return the result for query that is not paginated.
     *
     * @param Builder $query
     * @return mixed
     */
    protected function searchAll($query)
    {
        return $query->get();
    }

    /**
     * Is this a search for a singleton resource?
     *
     * @param Collection $filters
     * @return bool
     */
    protected function isSearchOne(Collection $filters)
    {
        return false;
    }

    /**
     * Return the result for a paginated query.
     *
     * @param Builder $query
     * @param EncodingParametersInterface $parameters
     * @return PageInterface
     */
    protected function paginate($query, EncodingParametersInterface $parameters)
    {
        if (!$this->paging) {
            throw new RuntimeException('Paging is not supported on adapter: ' . get_class($this));
        }

        /**
         * Set the key name on the strategy, so it knows what column is being used
         * for the resource's ID.
         *
         * @todo 2.0 add `withQualifiedKeyName` to the paging strategy interface.
         */
        if (method_exists($this->paging, 'withQualifiedKeyName')) {
            $this->paging->withQualifiedKeyName($this->getQualifiedKeyName());
        }

        return $this->paging->paginate($query, $parameters);
    }

    /**
     * Get the key that is used for the resource ID.
     *
     * @return string
     */
    protected function getKeyName()
    {
        return $this->primaryKey ?: $this->model->getRouteKeyName();
    }

    /**
     * @return string
     */
    protected function getQualifiedKeyName()
    {
        return $this->model->qualifyColumn($this->getKeyName());
    }

    /**
     * Get pagination parameters to use when the client has not provided paging parameters.
     *
     * @return array
     */
    protected function defaultPagination()
    {
        return (array) $this->defaultPagination;
    }

    /**
     * @param string|null $modelKey
     * @return BelongsTo
     */
    protected function belongsTo($modelKey = null)
    {
        return new BelongsTo($modelKey ?: $this->guessRelation());
    }

    /**
     * @param string|null $modelKey
     * @return HasOne
     */
    protected function hasOne($modelKey = null)
    {
        return new HasOne($modelKey ?: $this->guessRelation());
    }

    /**
     * @param string|null $modelKey
     * @return HasMany
     */
    protected function hasMany($modelKey = null)
    {
        return new HasMany($modelKey ?: $this->guessRelation());
    }

    /**
     * @param string|null $modelKey
     * @return HasManyThrough
     */
    protected function hasManyThrough($modelKey = null)
    {
        return new HasManyThrough($modelKey ?: $this->guessRelation());
    }

    /**
     * @param HasManyAdapterInterface ...$adapters
     * @return MorphHasMany
     */
    protected function morphMany(HasManyAdapterInterface ...$adapters)
    {
        return new MorphHasMany(...$adapters);
    }

    /**
     * @param \Closure $factory
     *      a factory that creates a new Eloquent query builder.
     * @return QueriesMany
     */
    protected function queriesMany(\Closure $factory)
    {
        return new QueriesMany($factory);
    }

    /**
     * @param \Closure $factory
     * @return QueriesOne
     */
    protected function queriesOne(\Closure $factory)
    {
        return new QueriesOne($factory);
    }

    /**
     * Default query execution used when querying records or relations.
     *
     * @param $query
     * @param EncodingParametersInterface $parameters
     * @return mixed
     */
    protected function queryAllOrOne($query, EncodingParametersInterface $parameters)
    {
        $filters = collect($parameters->getFilteringParameters());

        if ($this->isSearchOne($filters)) {
            return $this->queryOne($query, $parameters);
        }

        return $this->queryAll($query, $parameters);
    }

    /**
     * @param $query
     * @param EncodingParametersInterface $parameters
     * @return PageInterface|mixed
     */
    protected function queryAll($query, EncodingParametersInterface $parameters)
    {
        /** Apply eager loading */
        $this->with($query, $parameters);

        /** Filter */
        $this->applyFilters($query, collect($parameters->getFilteringParameters()));

        /** Sort */
        $this->sort($query, $parameters->getSortParameters());

        /** Paginate results if needed. */
        $pagination = collect($parameters->getPaginationParameters());

        return $pagination->isEmpty() ?
            $this->searchAll($query) :
            $this->paginate($query, $parameters);
    }

    /**
     * @param $query
     * @param EncodingParametersInterface $parameters
     * @return Model
     */
    protected function queryOne($query, EncodingParametersInterface $parameters)
    {
        $parameters = $this->getQueryParameters($parameters);

        /** Apply eager loading */
        $this->with($query, $parameters);

        /** Filter */
        $this->applyFilters($query, collect($parameters->getFilteringParameters()));

        /** Sort */
        $this->sort($query, $parameters->getSortParameters());

        return $this->searchOne($query);
    }

    /**
     * Get JSON API parameters to use when constructing an Eloquent query.
     *
     * This method is used to push in any default parameter values that should
     * be used if the client has not provided any.
     *
     * @param EncodingParametersInterface $parameters
     * @return EncodingParametersInterface
     */
    protected function getQueryParameters(EncodingParametersInterface $parameters)
    {
        return new EncodingParameters(
            $parameters->getIncludePaths(),
            $parameters->getFieldSets(),
            $parameters->getSortParameters() ?: $this->defaultSort(),
            $parameters->getPaginationParameters() ?: $this->defaultPagination(),
            $parameters->getFilteringParameters(),
            $parameters->getUnrecognizedParameters()
        );
    }

    /**
     * @return string
     */
    private function guessRelation()
    {
        list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);

        return $this->modelRelationForField($caller['function']);
    }

}


<?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;

class LaravelJsonApi
{

    /**
     * The default API name.
     *
     * @var null
     */
    public static $defaultApi = 'default';

    /**
     * Indicates if Laravel JSON API migrations will be run.
     *
     * @var bool
     */
    public static $runMigrations = false;

    /**
     * Indicates if listeners will be bound to the Laravel queue events.
     *
     * @var bool
     */
    public static $queueBindings = true;

    /**
     * Indicates if Laravel validator failed data is added to JSON API error objects.
     *
     * @var bool
     */
    public static $validationFailures = false;

    /**
     * Set the default API name.
     *
     * @param string $name
     * @return LaravelJsonApi
     */
    public static function defaultApi(string $name): self
    {
        if (empty($name)) {
            throw new \InvalidArgumentException('Default API name must not be empty.');
        }

        self::$defaultApi = $name;

        return new self();
    }

    /**
     * @return LaravelJsonApi
     */
    public static function runMigrations(): self
    {
        self::$runMigrations = true;

        return new self();
    }

    /**
     * @return LaravelJsonApi
     */
    public static function skipQueueBindings(): self
    {
        self::$queueBindings = false;

        return new self();
    }

    /**
     * @return LaravelJsonApi
     */
    public static function showValidatorFailures(): self
    {
        self::$validationFailures = true;

        return new self();
    }
}


<?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\Store;

use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceIdentifierInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\InvalidArgumentException;

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

    /**
     * @var array
     */
    private $map = [];

    /**
     * Add a record to the identity map for a resource identifier.
     *
     * The record can either be a boolean (the result of a store's `exists()` check), or the actual
     * record itself. However, a boolean cannot be inserted into the map if the map already holds the
     * record itself.
     *
     * @param ResourceIdentifierInterface $identifier
     * @param object|bool $record
     * @return $this
     */
    public function add(ResourceIdentifierInterface $identifier, $record)
    {
        if (!is_object($record) && !is_bool($record)) {
            throw new InvalidArgumentException('Expecting an object or a boolean to add to the identity map.');
        }

        $existing = $this->lookup($identifier);

        if (is_object($existing) && is_bool($record)) {
            throw new InvalidArgumentException('Attempting to push a boolean into the map in place of an object.');
        }

        $this->map[$identifier->toString()] = $record;

        return $this;
    }

    /**
     * Does the identity map know that ths supplied identifier exists?
     *
     * @param ResourceIdentifierInterface $identifier
     * @return bool|null
     *      the answer, or null if the identity map does not know
     */
    public function exists(ResourceIdentifierInterface $identifier)
    {
        $record = $this->lookup($identifier);

        return is_object($record) ? true : $record;
    }

    /**
     * Get the record from the identity map.
     *
     * @param ResourceIdentifierInterface $identifier
     * @return object|bool|null
     *      the record, false if it is known not to exist, or null if the identity map does not have the object.
     */
    public function find(ResourceIdentifierInterface $identifier)
    {
        $record = $this->lookup($identifier);

        if (false === $record) {
            return false;
        }

        return is_object($record) ? $record : null;
    }

    /**
     * @param ResourceIdentifierInterface $identifier
     * @return object|bool|null
     */
    private function lookup(ResourceIdentifierInterface $identifier)
    {
        $key = $identifier->toString();

        return isset($this->map[$key]) ? $this->map[$key] : 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\Http\Requests;

use CloudCreativity\LaravelJsonApi\Contracts\Http\DecoderInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\ResourceIdentifierInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use CloudCreativity\LaravelJsonApi\Http\Codec;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Contracts\Container\Container;
use Illuminate\Http\Request;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderParametersInterface;

/**
 * Class JsonApiRequest
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0
 */
class JsonApiRequest
{

    /**
     * @var Request
     */
    private $request;

    /**
     * @var Route
     */
    private $route;

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

    /**
     * @var HeaderParametersInterface|null
     */
    private $headers;

    /**
     * @var Codec|null
     */
    private $codec;

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

    /**
     * @var EncodingParametersInterface|null
     */
    private $parameters;

    /**
     * IlluminateRequest constructor.
     *
     * @param Request $request
     * @param Route $route
     * @param Container $container
     */
    public function __construct(
        Request $request,
        Route $route,
        Container $container
    ) {
        $this->request = $request;
        $this->route = $route;
        $this->container = $container;
    }

    /**
     * @return Route
     */
    public function getRoute(): Route
    {
        return $this->route;
    }

    /**
     * Get the domain record type that is subject of the request.
     *
     * @return string
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getType(): string
    {
        return $this->getRoute()->getType();
    }

    /**
     * What resource type does the request relate to?
     *
     * @return string|null
     *      the requested resource type, or null if none was requested.
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getResourceType(): ?string
    {
        return $this->getRoute()->getResourceType();
    }

    /**
     * What resource id does the request relate to?
     *
     * @return string|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getResourceId(): ?string
    {
        return $this->getRoute()->getResourceId();
    }

    /**
     * Get the resource identifier for the request.
     *
     * @return ResourceIdentifierInterface|null
     * @deprecated 2.0.0
     */
    public function getResourceIdentifier(): ?ResourceIdentifierInterface
    {
        return $this->getRoute()->getResourceIdentifier();
    }

    /**
     * Get the domain object that the request relates to.
     *
     * @return mixed|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getResource()
    {
        return $this->getRoute()->getResource();
    }

    /**
     * What resource relationship does the request relate to?
     *
     * @return string|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getRelationshipName(): ?string
    {
        return $this->getRoute()->getRelationshipName();
    }

    /**
     * What is the inverse resource type for a relationship?
     *
     * For example, a `GET /posts/1/author`, the string returned by this method
     * would be `users` if the related author is a `users` JSON API resource type.
     *
     * @return string|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getInverseResourceType(): ?string
    {
        return $this->getRoute()->getInverseResourceType();
    }

    /**
     * What process resource type does the request relate to?
     *
     * @return string|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getProcessType(): ?string
    {
        return $this->getRoute()->getProcessType();
    }

    /**
     * What process id does the request relate to?
     *
     * @return string|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getProcessId(): ?string
    {
        return $this->getRoute()->getProcessId();
    }

    /**
     * @return AsynchronousProcess|null
     * @deprecated 2.0.0 use `getRoute()`
     */
    public function getProcess(): ?AsynchronousProcess
    {
        return $this->getRoute()->getProcess();
    }

    /**
     * Get the process identifier for the request.
     *
     * @return ResourceIdentifierInterface|null
     * @deprecated 2.0.0
     */
    public function getProcessIdentifier(): ?ResourceIdentifierInterface
    {
        return $this->getRoute()->getProcessIdentifier();
    }

    /**
     * Get the encoding parameters from the request.
     *
     * @return EncodingParametersInterface
     */
    public function getParameters(): EncodingParametersInterface
    {
        if ($this->parameters) {
            return $this->parameters;
        }

        return $this->parameters = $this->container->make(EncodingParametersInterface::class);
    }

    /**
     * Is this an index request?
     *
     * E.g. `GET /posts`
     *
     * @return bool
     */
    public function isIndex(): bool
    {
        return $this->isMethod('get') &&
            $this->getRoute()->isNotResource() &&
            $this->getRoute()->isNotProcesses();
    }

    /**
     * Is this a create resource request?
     *
     * E.g. `POST /posts`
     *
     * @return bool
     */
    public function isCreateResource(): bool
    {
        return $this->isMethod('post') && $this->getRoute()->isNotResource();
    }

    /**
     * Is this a read resource request?
     *
     * E.g. `GET /posts/1`
     *
     * @return bool
     */
    public function isReadResource(): bool
    {
        return $this->isMethod('get') &&
            $this->getRoute()->isResource() &&
            $this->getRoute()->isNotRelationship();
    }

    /**
     * Is this an update resource request?
     *
     * E.g. `PATCH /posts/1`
     *
     * @return bool
     */
    public function isUpdateResource(): bool
    {
        return $this->isMethod('patch') &&
            $this->getRoute()->isResource() &&
            $this->getRoute()->isNotRelationship();
    }

    /**
     * Is this a delete resource request?
     *
     * E.g. `DELETE /posts/1`
     *
     * @return bool
     */
    public function isDeleteResource(): bool
    {
        return $this->isMethod('delete') &&
            $this->getRoute()->isResource() &&
            $this->getRoute()->isNotRelationship();
    }

    /**
     * Is this a request for a related resource or resources?
     *
     * E.g. `GET /posts/1/author` or `GET /posts/1/comments`
     *
     * @return bool
     */
    public function isReadRelatedResource(): bool
    {
        return $this->getRoute()->isRelationship() && !$this->hasRelationships();
    }

    /**
     * Does the request URI have the 'relationships' keyword?
     *
     * E.g. `/posts/1/relationships/author` or `/posts/1/relationships/comments`
     *
     * I.e. the URL request contains the pattern `/relationships/` after the
     * resource id and before the relationship name.
     *
     * @return bool
     * @see http://jsonapi.org/format/#fetching-relationships
     */
    public function hasRelationships(): bool
    {
        return $this->request->is('*/relationships/*');
    }

    /**
     * Is this a request to read the data of a relationship?
     *
     * E.g. `GET /posts/1/relationships/author` or `GET /posts/1/relationships/comments`
     *
     * @return bool
     */
    public function isReadRelationship(): bool
    {
        return $this->isMethod('get') && $this->hasRelationships();
    }

    /**
     * Is this a request to modify the data of a relationship?
     *
     * I.e. is this a replace relationship, add to relationship or remove from relationship
     * request.
     *
     * @return bool
     */
    public function isModifyRelationship(): bool
    {
        return $this->isReplaceRelationship() ||
            $this->isAddToRelationship() ||
            $this->isRemoveFromRelationship();
    }

    /**
     * Is this a request to replace the data of a relationship?
     *
     * E.g. `PATCH /posts/1/relationships/author` or `PATCH /posts/1/relationships/comments`
     */
    public function isReplaceRelationship(): bool
    {
        return $this->isMethod('patch') && $this->hasRelationships();
    }

    /**
     * Is this a request to add to the data of a has-many relationship?
     *
     * E.g. `POST /posts/1/relationships/comments`
     *
     * @return bool
     */
    public function isAddToRelationship(): bool
    {
        return $this->isMethod('post') && $this->hasRelationships();
    }

    /**
     * Is this a request to remove from the data of a has-many relationship?
     *
     * E.g. `DELETE /posts/1/relationships/comments`
     *
     * @return bool
     */
    public function isRemoveFromRelationship(): bool
    {
        return $this->isMethod('delete') && $this->hasRelationships();
    }

    /**
     * Is this a request to read all processes for a resource type?
     *
     * E.g. `GET /posts/queue-jobs`
     *
     * @return bool
     */
    public function isReadProcesses(): bool
    {
        return $this->isMethod('get') &&
            $this->getRoute()->isProcesses() &&
            $this->getRoute()->isNotProcess();
    }

    /**
     * Is this a request to read a process for a resource type?
     *
     * E.g. `GET /posts/queue-jobs/839765f4-7ff4-4625-8bf7-eecd3ab44946`
     *
     * @return bool
     */
    public function isReadProcess(): bool
    {
        return $this->isMethod('get') && $this->getRoute()->isProcess();
    }

    /**
     * Is the HTTP request method the one provided?
     *
     * @param string $method
     *      the expected method - case insensitive.
     * @return bool
     */
    private function isMethod($method): bool
    {
        return $this->request->isMethod($method);
    }

}


<?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\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Collection;

/**
 * Class AllowedFieldSets
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class AllowedFieldSets implements Rule
{

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

    /**
     * @var Collection|null
     */
    private $allowed;

    /**
     * The last value that was validated.
     *
     * @var array|null
     */
    private $value;

    /**
     * AllowedFieldSets constructor.
     *
     * @param array|null $allowed
     */
    public function __construct(array $allowed = null)
    {
        $this->all = is_null($allowed);
        $this->allowed = collect($allowed);
    }

    /**
     * Allow fields for a resource type.
     *
     * @param string $resourceType
     * @param string[]|null $fields
     *      the allowed fields, empty array for none allowed, or null for all allowed.
     * @return $this
     */
    public function allow(string $resourceType, array $fields = null): self
    {
        $this->all = false;
        $this->allowed[$resourceType] = $fields;

        return $this;
    }

    /**
     * Allow any fields for the specified resource type.
     *
     * @param string ...$resourceTypes
     * @return $this
     */
    public function any(string ...$resourceTypes): self
    {
        foreach ($resourceTypes as $resourceType) {
            $this->allow($resourceType, null);
        }

        return $this;
    }

    /**
     * Allow no fields for the specified resource type.
     *
     * @param string ...$resourceTypes
     * @return $this
     */
    public function none(string ...$resourceTypes): self
    {
        foreach ($resourceTypes as $resourceType) {
            $this->allow($resourceType, []);
        }

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function passes($attribute, $value)
    {
        $this->value = $value;

        if ($this->all) {
            return true;
        }

        if (!is_array($value)) {
            return false;
        }

        return collect($value)->every(function ($value, $key) {
            return $this->allowed($key, (string) $value);
        });
    }

    /**
     * @inheritDoc
     */
    public function message()
    {
        $invalid = $this->invalid();

        if ($invalid->isEmpty()) {
            $key = 'default';
        } else {
            $key = (1 === $invalid->count()) ? 'singular' : 'plural';
        }

        return trans("jsonapi::validation.allowed_field_sets.{$key}", [
            'values' => $invalid->implode(', '),
        ]);
    }

    /**
     * Are the fields allowed for the specified resource type?
     *
     * @param string $resourceType
     * @param string $fields
     * @return bool
     */
    protected function allowed(string $resourceType, string $fields): bool
    {
        return $this->notAllowed($resourceType, $fields)->isEmpty();
    }

    /**
     * Get the invalid fields for the resource type.
     *
     * @param string $resourceType
     * @param string $fields
     * @return Collection
     */
    protected function notAllowed(string $resourceType, string $fields): Collection
    {
        $fields = collect(explode(',', $fields));

        if (!$this->allowed->has($resourceType)) {
            return $fields;
        }

        $allowed = $this->allowed->get($resourceType);

        if (is_null($allowed)) {
            return collect();
        }

        $allowed = collect((array) $allowed);

        return $fields->reject(function ($value) use ($allowed) {
            return $allowed->contains($value);
        });
    }

    /**
     * Get the fields that are invalid.
     *
     * @return Collection
     */
    protected function invalid(): Collection
    {
        if (!is_array($this->value)) {
            return collect();
        }

        return collect($this->value)->map(function ($value, $key) {
            return $this->notAllowed($key, $value);
        })->flatMap(function (Collection $fields, $type) {
            return $fields->map(function ($field) use ($type) {
                return "{$type}.{$field}";
            });
        });
    }

}


<?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\Eloquent\Concerns;

use CloudCreativity\LaravelJsonApi\Utils\Str;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\SortParameterInterface;
use Neomerx\JsonApi\Encoder\Parameters\SortParameter;

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

    /**
     * The default sorting to use if no sort parameters have been provided.
     *
     * Used when the client does not provide any sort parameters. Use
     * either a string for a single default sort field, or an array
     * of strings for multiple default sort fields.
     *
     * As per the JSON API convention, sort parameters prefixed with
     * a `-` denoted descending order.
     *
     * For example, if `name` ascending is the default:
     *
     * ```
     * protected $defaultSort = 'name';
     * ```
     *
     * Or if `created-at` descending, then `name` ascending is the default:
     *
     * ```
     * protected $defaultSort = ['-created-at`, 'name'];
     * ```
     *
     * @var string|string[]
     */
    protected $defaultSort = [];

    /**
     * A mapping of sort parameters to columns.
     *
     * Use this to map any parameters to columns where the two are not identical. E.g. if
     * your sort param is called `category` but the column to use is `type`, then set this
     * property to `['category' => 'type']`.
     *
     * @var array
     */
    protected $sortColumns = [];

    /**
     * Apply sort parameters to the query.
     *
     * @param Builder $query
     * @param SortParameterInterface[] $sortBy
     * @return void
     */
    protected function sort($query, array $sortBy)
    {
        /** @var SortParameterInterface $param */
        foreach ($sortBy as $param) {
            $this->sortBy($query, $param);
        }
    }

    /**
     * Get sort parameters to use when the client has not provided a sort order.
     *
     * @return array
     */
    protected function defaultSort()
    {
        return collect($this->defaultSort)->map(function ($param) {
            $desc = ($param[0] === '-');
            $field = ltrim($param, '-');

            return new SortParameter($field, !$desc);
        })->all();
    }

    /**
     * @param Builder $query
     * @param SortParameterInterface $param
     */
    protected function sortBy($query, SortParameterInterface $param)
    {
        $column = $this->getQualifiedSortColumn($query, $param->getField());
        $order = $param->isAscending() ? 'asc' : 'desc';

        $query->orderBy($column, $order);
    }

    /**
     * @param Builder $query
     * @param string $field
     * @return string
     */
    protected function getQualifiedSortColumn($query, $field)
    {
        $key = $this->getSortColumn($field, $query->getModel());

        return $query->qualifyColumn($key);
    }

    /**
     * Get the table column to use for the specified search field.
     *
     * @param string $field
     * @param Model $model
     * @return string
     */
    protected function getSortColumn($field, Model $model)
    {
        /** If there is a custom mapping, return that */
        if (isset($this->sortColumns[$field])) {
            return $this->sortColumns[$field];
        }

        return $model::$snakeAttributes ? Str::underscore($field) : Str::camelize($field);
    }

}


<?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 CloudCreativity\LaravelJsonApi\Api\Api;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Support\Arr;

/**
 * Class ApiRegistration
 *
 * @package CloudCreativity\LaravelJsonApi
 */
final class ApiRegistration
{

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

    /**
     * @var Api
     */
    private $api;

    /**
     * JSON API options.
     *
     * @var array
     */
    private $options;

    /**
     * Laravel route attributes.
     *
     * @var array
     */
    private $attributes;

    /**
     * ApiRegistration constructor.
     *
     * @param Registrar $routes
     * @param Api $api
     * @param array $options
     */
    public function __construct(Registrar $routes, Api $api, array $options = [])
    {
        // this maintains compatibility with passing attributes and options through as a single array.
        $attrs = ['content-negotiator', 'processes', 'prefix', 'id'];

        $this->routes = $routes;
        $this->api = $api;
        $this->options = collect($options)->only($attrs)->all();
        $this->attributes = collect($options)->forget($attrs)->all();
    }

    /**
     * @param string $constraint
     * @return $this
     */
    public function defaultId(string $constraint): self
    {
        $this->options['id'] = $constraint;

        return $this;
    }

    /**
     * @param string $controller
     * @return ApiRegistration
     */
    public function defaultController(string $controller): self
    {
        $this->options['controller'] = $controller;

        return $this;
    }

    /**
     * Set the default content negotiator.
     *
     * @param string $negotiator
     * @return $this
     */
    public function defaultContentNegotiator(string $negotiator): self
    {
        $this->options['content-negotiator'] = $negotiator;

        return $this;
    }

    /**
     * Set an authorizer for the entire API.
     *
     * @param string $authorizer
     * @return $this
     */
    public function authorizer(string $authorizer): self
    {
        return $this->middleware("json-api.auth:{$authorizer}");
    }

    /**
     * Add middleware.
     *
     * @param string ...$middleware
     * @return $this
     */
    public function middleware(string ...$middleware): self
    {
        $this->attributes['middleware'] = array_merge(
            Arr::wrap($this->attributes['middleware'] ?? []),
            $middleware
        );

        return $this;
    }

    /**
     * @param string $domain
     * @return ApiRegistration
     */
    public function domain(string $domain): self
    {
        $this->attributes['domain'] = $domain;

        return $this;
    }

    /**
     * @param string $namespace
     * @return $this
     */
    public function withNamespace(string $namespace): self
    {
        $this->attributes['namespace'] = $namespace;

        return $this;
    }

    /**
     * @param \Closure $callback
     */
    public function routes(\Closure $callback): void
    {
        $this->routes->group($this->attributes(), function () use ($callback) {
            $group = new RouteRegistrar($this->routes, $this->options());
            $callback($group, $this->routes);
            $this->api->providers()->mountAll($group, $this->routes);
        });
    }

    /**
     * @return array
     */
    private function attributes(): array
    {
        $url = $this->api->getUrl();

        return collect($this->attributes)->merge([
            'as' => $url->getName(),
            'prefix' => $url->getNamespace(),
            'middleware' => $this->allMiddleware()
        ])->all();
    }

    /**
     * @return array
     */
    private function options(): array
    {
        return array_merge([
            'processes' => $this->api->getJobs()->getResource(),
        ], $this->options);
    }

    /**
     * @return array
     */
    private function allMiddleware(): array
    {
        return collect(["json-api:{$this->api->getName()}"])
            ->merge(Arr::wrap($this->attributes['middleware'] ?? []))
            ->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\Queue;

use CloudCreativity\LaravelJsonApi\Contracts\Queue\AsynchronousProcess;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;

class UpdateClientProcess
{

    /**
     * Handle the event.
     *
     * @param JobProcessed|JobFailed $event
     * @return void
     */
    public function handle($event): void
    {
        if (!$job = $this->deserialize($event->job)) {
            return;
        }

        $clientJob = $job->clientJob ?? null;

        if (!$clientJob instanceof AsynchronousProcess) {
            return;
        }

        $clientJob->processed($event->job);
    }

    /**
     * @param Job $job
     * @return mixed|null
     */
    private function deserialize(Job $job)
    {
        $data = $this->payload($job)['data'] ?? [];
        $command = $data['command'] ?? null;

        return is_string($command) ? unserialize($command) : null;
    }

    /**
     * @param Job $job
     * @return array
     */
    private function payload(Job $job): array
    {
        return json_decode($job->getRawBody(), 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\Routing;

use Illuminate\Routing\Route;
use Illuminate\Routing\Router;
use Illuminate\Routing\RouteRegistrar as IlluminateRegistrar;
use Illuminate\Support\Str;

/**
 * Class CustomRegistration
 *
 * @package CloudCreativity\LaravelJsonApi
 */
final class RouteRegistration extends IlluminateRegistrar
{

    /**
     * @var RouteRegistrar
     */
    private $registrar;

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

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

    /**
     * CustomRegistration constructor.
     *
     * @param Router $router
     * @param RouteRegistrar $registrar
     * @param array $defaults
     */
    public function __construct(Router $router, RouteRegistrar $registrar, array $defaults = [])
    {
        parent::__construct($router);
        $this->registrar = $registrar;
        $this->defaults = $defaults;
    }

    /**
     * Set the controller for the route.
     *
     * @param string $controller
     * @return $this
     */
    public function controller(string $controller): self
    {
        $this->controller = $controller;

        return $this;
    }

    /**
     * Set the route's relationship field name and inverse resource type.
     *
     * @param string $field
     * @param string|null $inverse
     * @return $this
     */
    public function field(string $field, string $inverse = null): self
    {
        $this->defaults = array_merge($this->defaults, [
            ResourceRegistrar::PARAM_RELATIONSHIP_NAME => $field,
            ResourceRegistrar::PARAM_RELATIONSHIP_INVERSE_TYPE => $inverse ?: Str::plural($field),
        ]);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function match($methods, $uri, $action = null)
    {
        return $this->setupRoute(
            parent::match($methods, $uri, $action)
        );
    }

    /**
     * @inheritdoc
     */
    public function group($callback)
    {
        if ($callback instanceof \Closure) {
            $callback = function () use ($callback) {
                $callback($this->registrar);
            };
        }

        parent::group($callback);
    }

    /**
     * @inheritdoc
     */
    protected function registerRoute($method, $uri, $action = null)
    {
        return $this->setupRoute(
            parent::registerRoute($method, $uri, $action)
        );
    }

    /**
     * @inheritdoc
     */
    protected function compileAction($action)
    {
        $action = parent::compileAction($action);
        $uses = $action['uses'] ?? null;

        if (is_string($uses) && $this->controller && !Str::contains($uses, '@')) {
            $action['uses'] = $this->controller . '@' . $uses;
        }

        return $action;
    }

    /**
     * @param Route $route
     * @return Route
     */
    private function setupRoute(Route $route): Route
    {
        $route->defaults = $this->defaults;

        /** If there is no resource type, we need to prepend content negotiation. */
        if (!isset($this->defaults[ResourceRegistrar::PARAM_RESOURCE_TYPE])) {
            $cn = $this->options['content-negotiator'] ?? '';
            $cn = $cn ? "json-api.content:{$cn}" : 'json-api.content';

            $route->middleware(
                collect($route->middleware())->prepend($cn)->unique()->all()
            );
        }

        return $route;
    }
}


<?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\AcceptMediaTypeInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Encoder\EncoderOptions;
use Neomerx\JsonApi\Http\Headers\MediaType;

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

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

    /**
     * @var EncoderOptions|null
     */
    private $options;

    /**
     * Create an encoding that will encode JSON API content.
     *
     * @param string|MediaTypeInterface $mediaType
     * @param int $options
     * @param string|null $urlPrefix
     * @param int $depth
     * @return Encoding
     */
    public static function create(
        $mediaType,
        int $options = 0,
        string $urlPrefix = null,
        int $depth = 512
    ): self
    {
        if (!$mediaType instanceof MediaTypeInterface) {
            $mediaType = MediaType::parse(0, $mediaType);
        }

        return new self($mediaType, new EncoderOptions($options, $urlPrefix, $depth));
    }

    /**
     * Create an encoding for the JSON API media type.
     *
     * @param int $options
     * @param string|null $urlPrefix
     * @param int $depth
     * @return Encoding
     */
    public static function jsonApi(int $options = 0, string $urlPrefix = null, int $depth = 512): self
    {
        return self::create(
            MediaTypeInterface::JSON_API_MEDIA_TYPE,
            $options,
            $urlPrefix,
            $depth
        );
    }

    /**
     * Create an encoding that will not encode JSON API content.
     *
     * @param string|MediaTypeInterface $mediaType
     * @return Encoding
     */
    public static function custom($mediaType): self
    {
        if (!$mediaType instanceof MediaTypeInterface) {
            $mediaType = MediaType::parse(0, $mediaType);
        }

        return new self($mediaType, null);
    }

    /**
     * @param $key
     * @param $value
     * @param string|null $urlPrefix
     * @return Encoding
     */
    public static function fromArray($key, $value, string $urlPrefix = null)
    {
        if (is_numeric($key)) {
            $key = $value;
            $value = 0;
        }

        return (false === $value) ? self::custom($key) : self::create($key, $value, $urlPrefix);
    }

    /**
     * Encoding constructor.
     *
     * @param MediaTypeInterface $mediaType
     * @param EncoderOptions|null $options
     *      the encoding options, if the encoding to JSON API content is supported.
     */
    public function __construct(MediaTypeInterface $mediaType, ?EncoderOptions $options)
    {
        $this->mediaType = $mediaType;
        $this->options = $options;
    }

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

    /**
     * Get the options, if the media type returns JSON API encoded content.
     *
     * @return EncoderOptions|null
     */
    public function getOptions(): ?EncoderOptions
    {
        return $this->options;
    }

    /**
     * Will the encoding encode JSON API content?
     *
     * @return bool
     */
    public function hasOptions(): bool
    {
        return !is_null($this->options);
    }

    /**
     * Is the encoding for any of the supplied media types?
     *
     * @param string ...$mediaTypes
     * @return bool
     */
    public function is(string ...$mediaTypes): bool
    {
        $mediaTypes = collect($mediaTypes)->map(function ($mediaType, $index) {
            return MediaType::parse($index, $mediaType);
        });

        return $this->any(...$mediaTypes);
    }

    /**
     * @param MediaTypeInterface ...$mediaTypes
     * @return bool
     */
    public function any(MediaTypeInterface ...$mediaTypes): bool
    {
        foreach ($mediaTypes as $mediaType) {
            if ($this->matchesTo($mediaType)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Does the encoding match the supplied media type?
     *
     * @param MediaTypeInterface $mediaType
     * @return bool
     */
    public function matchesTo(MediaTypeInterface $mediaType): bool
    {
        return $this->getMediaType()->matchesTo($mediaType);
    }

    /**
     * Is the encoding acceptable?
     *
     * @param AcceptMediaTypeInterface $mediaType
     * @return bool
     */
    public function accept(AcceptMediaTypeInterface $mediaType): bool
    {
        // if quality factor 'q' === 0 it means this type is not acceptable (RFC 2616 #3.9)
        if (0 === $mediaType->getQuality()) {
            return false;
        }

        return $this->matchesTo($mediaType);
    }

}


<?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;

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

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

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

    /**
     * @var int
     */
    private $limit;

    /**
     * Create a cursor from query parameters.
     *
     * @param array $parameters
     * @param string $beforeKey
     * @param string $afterKey
     * @param string $limitKey
     * @return Cursor
     */
    public static function create(
        array $parameters,
        $beforeKey = 'before',
        $afterKey = 'after',
        $limitKey = 'limit'
    ) {
        return new self(
            array_get($parameters, $beforeKey),
            array_get($parameters, $afterKey),
            array_get($parameters, $limitKey, 15)
        );
    }

    /**
     * Cursor constructor.
     *
     * @param null $before
     * @param null $after
     * @param int $limit
     */
    public function __construct($before = null, $after = null, $limit = 15)
    {
        $this->before = $before ?: null;
        $this->after = $after ?: null;
        $this->limit = 0 < $limit ? (int) $limit : 1;
    }

    /**
     * @return bool
     */
    public function isBefore()
    {
        return !is_null($this->before);
    }

    /**
     * @return string|null
     */
    public function getBefore()
    {
        return $this->before;
    }

    /**
     * @return bool
     */
    public function isAfter()
    {
        return !is_null($this->after) && !$this->isBefore();
    }

    /**
     * @return string|null
     */
    public function getAfter()
    {
        return $this->after;
    }

    /**
     * @return int
     */
    public function getLimit()
    {
        return $this->limit;
    }

}


<?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;

use CloudCreativity\LaravelJsonApi\Api\Repository;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Exceptions\ExceptionParserInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Factories\FactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Repositories\ErrorRepositoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Resolver\ResolverInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Store\StoreInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\ExceptionParser;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Http\Middleware\Authorize;
use CloudCreativity\LaravelJsonApi\Http\Middleware\BootJsonApi;
use CloudCreativity\LaravelJsonApi\Http\Middleware\NegotiateContent;
use CloudCreativity\LaravelJsonApi\Http\Requests\JsonApiRequest;
use CloudCreativity\LaravelJsonApi\Queue\UpdateClientProcess;
use CloudCreativity\LaravelJsonApi\Routing\JsonApiRegistrar;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use CloudCreativity\LaravelJsonApi\Services\JsonApiService;
use CloudCreativity\LaravelJsonApi\View\Renderer;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
use Neomerx\JsonApi\Contracts\Document\DocumentFactoryInterface;
use Neomerx\JsonApi\Contracts\Encoder\Handlers\HandlerFactoryInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parser\ParserFactoryInterface;
use Neomerx\JsonApi\Contracts\Encoder\Stack\StackFactoryInterface;
use Neomerx\JsonApi\Contracts\Factories\FactoryInterface as NeomerxFactoryInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderParametersParserInterface;
use Neomerx\JsonApi\Contracts\Http\HttpFactoryInterface;
use Neomerx\JsonApi\Contracts\Http\Query\QueryParametersParserInterface;
use Neomerx\JsonApi\Contracts\Schema\SchemaFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;

/**
 * Class ServiceProvider
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class ServiceProvider extends BaseServiceProvider
{

    /**
     * @param Router $router
     */
    public function boot(Router $router)
    {
        $this->bootMiddleware($router);
        $this->bootResponseMacro();
        $this->bootBladeDirectives();
        $this->bootTranslations();

        if (LaravelJsonApi::$queueBindings) {
            Queue::after(UpdateClientProcess::class);
            Queue::failing(UpdateClientProcess::class);
        }

        if ($this->app->runningInConsole()) {
            $this->bootMigrations();

            $this->publishes([
                __DIR__ . '/../database/migrations' => database_path('migrations'),
            ], 'json-api-migrations');

            $this->commands([
                Console\Commands\MakeAdapter::class,
                Console\Commands\MakeApi::class,
                Console\Commands\MakeAuthorizer::class,
                Console\Commands\MakeContentNegotiator::class,
                Console\Commands\MakeResource::class,
                Console\Commands\MakeSchema::class,
                Console\Commands\MakeValidators::class,
            ]);
        }
    }

    /**
     * Register JSON API services.
     *
     * @return void
     */
    public function register()
    {
        $this->bindNeomerx();
        $this->bindService();
        $this->bindInboundRequest();
        $this->bindRouteRegistrar();
        $this->bindApiRepository();
        $this->bindExceptionParser();
        $this->bindRenderer();
        $this->mergePackageConfig();
    }

    /**
     * Register package middleware.
     *
     * @param Router $router
     */
    protected function bootMiddleware(Router $router)
    {
        $router->aliasMiddleware('json-api', BootJsonApi::class);
        $router->aliasMiddleware('json-api.content', NegotiateContent::class);
        $router->aliasMiddleware('json-api.auth', Authorize::class);
    }

    /**
     * Register package translations.
     *
     * @return void
     */
    protected function bootTranslations()
    {
        $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'jsonapi');
    }

    /**
     * Register a response macro.
     *
     * @return void
     */
    protected function bootResponseMacro()
    {
        Response::macro('jsonApi', function ($api = null) {
            return json_api($api)->getResponses()->withEncodingParameters(
                app(EncodingParametersInterface::class)
            );
        });
    }

    /**
     * Register Blade directives.
     */
    protected function bootBladeDirectives()
    {
        /** @var BladeCompiler $compiler */
        $compiler = $this->app->make(BladeCompiler::class);
        $compiler->directive('jsonapi', Renderer::class . '::compileWith');
        $compiler->directive('encode', Renderer::class . '::compileEncode');
    }

    /**
     * Register package migrations.
     *
     * @return void
     */
    protected function bootMigrations()
    {
        if (LaravelJsonApi::$runMigrations) {
            $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
        }
    }

    /**
     * Bind parts of the neomerx/json-api dependency into the service container.
     *
     * For this Laravel JSON API package, we use our extended JSON API factory.
     * This ensures that we can override any parts of the Neomerx JSON API package
     * that we want.
     *
     * As the Neomerx package splits the factories into multiple interfaces, we
     * also register aliases for each of the factory interfaces.
     *
     * The Neomerx package allows a logger to be injected into the factory. This
     * enables the Neomerx package to log messages. When creating the factory, we
     * therefore set the logger as our application's logger.
     *
     * @return void
     */
    protected function bindNeomerx()
    {
        $this->app->singleton(Factory::class, function (Application $app) {
            $factory = new Factory($app);
            $factory->setLogger($app->make(LoggerInterface::class));
            return $factory;
        });

        $this->app->alias(Factory::class, FactoryInterface::class);
        $this->app->alias(Factory::class, NeomerxFactoryInterface::class);
        $this->app->alias(Factory::class, DocumentFactoryInterface::class);
        $this->app->alias(Factory::class, HandlerFactoryInterface::class);
        $this->app->alias(Factory::class, HttpFactoryInterface::class);
        $this->app->alias(Factory::class, ParserFactoryInterface::class);
        $this->app->alias(Factory::class, SchemaFactoryInterface::class);
        $this->app->alias(Factory::class, StackFactoryInterface::class);
    }

    /**
     * Bind the JSON API service as a singleton.
     */
    protected function bindService()
    {
        $this->app->singleton(JsonApiService::class);
        $this->app->alias(JsonApiService::class, 'json-api');
        $this->app->alias(JsonApiService::class, 'json-api.service');
    }

    /**
     * Bind an alias for the route registrar.
     */
    protected function bindRouteRegistrar()
    {
        $this->app->alias(JsonApiRegistrar::class, 'json-api.registrar');
    }

    /**
     * Bind the inbound request services so they can be type-hinted in controllers and authorizers.
     *
     * @return void
     */
    protected function bindInboundRequest()
    {
        $this->app->singleton(JsonApiRequest::class);
        $this->app->alias(JsonApiRequest::class, 'json-api.request');

        $this->app->singleton(Route::class, function (Application $app) {
            return new Route(
                $app->make(ResolverInterface::class),
                $app->make('router')->current()
            );
        });

        $this->app->bind(StoreInterface::class, function () {
            return json_api()->getStore();
        });

        $this->app->bind(ResolverInterface::class, function () {
            return json_api()->getResolver();
        });

        $this->app->bind(ErrorRepositoryInterface::class, function () {
            return json_api()->getErrors();
        });

        $this->app->bind(ContainerInterface::class, function () {
            return json_api()->getContainer();
        });

        $this->app->singleton(HeaderParametersInterface::class, function (Application $app) {
            /** @var HeaderParametersParserInterface $parser */
            $parser = $app->make(HttpFactoryInterface::class)->createHeaderParametersParser();
            /** @var ServerRequestInterface $serverRequest */
            $serverRequest = $app->make(ServerRequestInterface::class);

            return $parser->parse($serverRequest, http_contains_body($serverRequest));
        });

        $this->app->singleton(EncodingParametersInterface::class, function (Application $app) {
            /** @var QueryParametersParserInterface $parser */
            $parser = $app->make(HttpFactoryInterface::class)->createQueryParametersParser();

            return $parser->parseQueryParameters(
                request()->query()
            );
        });
    }

    /**
     * Bind the API repository as a singleton.
     */
    protected function bindApiRepository()
    {
        $this->app->singleton(Repository::class);
    }

    /**
     * Bind the exception parser into the service container.
     */
    protected function bindExceptionParser()
    {
        $this->app->singleton(ExceptionParserInterface::class, ExceptionParser::class);
        $this->app->alias(ExceptionParserInterface::class, 'json-api.exceptions');
    }

    /**
     * Bind the view renderer into the service container.
     */
    protected function bindRenderer()
    {
        $this->app->singleton(Renderer::class);
        $this->app->alias(Renderer::class, 'json-api.renderer');
    }

    /**
     * Merge default package config.
     */
    protected function mergePackageConfig()
    {
        $this->mergeConfigFrom(__DIR__ . '/../config/json-api-errors.php', 'json-api-errors');
    }

    /**
     * @param $key
     * @param $default
     * @return array
     */
    protected function getConfig($key, $default = null)
    {
        $key = sprintf('%s.%s', 'json-api', $key);

        return config($key, $default);
    }

    /**
     * @return array
     */
    protected function getErrorConfig()
    {
        return (array) config('json-api-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\Validators;

use CloudCreativity\LaravelJsonApi\Utils\ErrorsAwareTrait;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Contracts\Validation\Validator;

/**
 * Class AbstractValidator
 *
 * @package CloudCreativity\LaravelJsonApi
 * @deprecated 2.0.0 use classes in the `Validation` namespace instead.
 */
abstract class AbstractValidator
{

    use ErrorsAwareTrait;

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

    /**
     * Get validation rules.
     *
     * @return array
     */
    abstract protected function getRules();

    /**
     * Get custom messages for validator errors.
     *
     * @return array
     */
    abstract protected function getMessages();

    /**
     * Get custom attributes for validator errors.
     *
     * @return array
     */
    abstract protected function getAttributes();

    /**
     * @param Validator $validator
     * @return void
     */
    abstract protected function configureValidator(Validator $validator);

    /**
     * AttributesValidator constructor.
     *
     * @param Factory $validatorFactory
     */
    public function __construct(Factory $validatorFactory)
    {
        $this->validatorFactory = $validatorFactory;
    }

    /**
     * Create a validator instance to validate the supplied data.
     *
     * @param array $data
     *      the data to validate.
     * @return Validator
     */
    protected function make(array $data)
    {
        $validator = $this->validatorFactory->make(
            $data,
            $this->getRules(),
            $this->getMessages(),
            $this->getAttributes()
        );

        $this->configureValidator($validator);

        return $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\Concerns;

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

    /**
     * JSON API fields that are fillable into a record.
     *
     * @var string[]
     */
    protected $fillable = [];

    /**
     * JSON API fields to skip when filling a record with values from a resource.
     *
     * @var string[]
     */
    protected $guarded = [];

    /**
     * Is the JSON API field allowed to be filled into the supplied record?
     *
     * @param $field
     * @param $record
     * @return bool
     */
    protected function isFillable($field, $record)
    {
        /** If the field is listed in the fillable fields, it can be filled. */
        if (in_array($field, $fillable = $this->getFillable($record))) {
            return true;
        }

        /** If the field is listed in the guarded fields, it cannot be filled. */
        if ($this->isGuarded($field, $record)) {
            return false;
        }

        /** Otherwise we can fill if everything is fillable. */
        return empty($fillable);
    }

    /**
     * Is the JSON API field not allowed to be filled into the supplied record?
     *
     * @param $field
     * @param $record
     * @return bool
     */
    protected function isNotFillable($field, $record)
    {
        return !$this->isFillable($field, $record);
    }

    /**
     * Is the JSON API field to be ignored when filling the supplied record?
     *
     * @param $field
     * @param $record
     * @return bool
     */
    protected function isGuarded($field, $record)
    {
        return in_array($field, $this->getGuarded($record));
    }

    /**
     * Get the JSON API fields that are allowed to be filled into a record.
     *
     * @param $record
     * @return string[]
     */
    protected function getFillable($record)
    {
        return $this->fillable;
    }

    /**
     * Get the JSON API fields to skip when filling the supplied record.
     *
     * @param $record
     * @return string[]
     */
    protected function getGuarded($record)
    {
        return $this->guarded;
    }

}


<?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\Requests;

use CloudCreativity\LaravelJsonApi\Contracts\Auth\AuthorizerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\ContainerInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Object\DocumentInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\DocumentValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorFactoryInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Validators\ValidatorProviderInterface;
use CloudCreativity\LaravelJsonApi\Exceptions\DocumentRequiredException;
use CloudCreativity\LaravelJsonApi\Exceptions\ValidationException;
use CloudCreativity\LaravelJsonApi\Factories\Factory;
use CloudCreativity\LaravelJsonApi\Object\Document;
use CloudCreativity\LaravelJsonApi\Routing\Route;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Validation\ValidatesWhenResolved;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Exceptions\JsonApiException;

abstract class ValidatedRequest implements ValidatesWhenResolved
{

    /**
     * @var Request
     */
    protected $request;

    /**
     * @var Route
     */
    protected $route;

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

    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var array|null
     */
    private $data;

    /**
     * @var EncodingParametersInterface|null
     */
    private $parameters;

    /**
     * Authorize the request.
     *
     * @return void
     * @throws AuthenticationException
     * @throws AuthorizationException
     */
    abstract protected function authorize();

    /**
     * Validate the query parameters.
     *
     * @return void
     * @throws JsonApiException
     */
    abstract protected function validateQuery();

    /**
     * ValidatedRequest constructor.
     *
     * @param Request $httpRequest
     * @param ContainerInterface $container
     * @param Factory $factory
     * @param Route $route
     */
    public function __construct(
        Request $httpRequest,
        ContainerInterface $container,
        Factory $factory,
        Route $route
    ) {
        $this->request = $httpRequest;
        $this->factory = $factory;
        $this->container = $container;
        $this->route = $route;
    }

    /**
     * Get an item from the JSON API document using "dot" notation.
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public function get($key, $default = null)
    {
        return array_get($this->all(), $key, $default);
    }

    /**
     * Get the JSON API document as an array.
     *
     * @return array
     */
    public function all()
    {
        if (is_array($this->data)) {
            return $this->data;
        }

        return $this->data = $this->route->getCodec()->all($this->request);
    }

    /**
     * @param $key
     * @return UploadedFile|null
     */
    public function file($key): ?UploadedFile
    {
        $file = $this->get($key);

        return ($file instanceof UploadedFile) ? $file : null;
    }

    /**
     * Get parsed query parameters.
     *
     * @return array
     */
    public function query()
    {
        return $this->request->query();
    }

    /**
     * Get the JSON API document as an object.
     *
     * @return object
     */
    public function decode()
    {
        return $this->route
            ->getCodec()
            ->document($this->request);
    }

    /**
     * Get the JSON API document as an object.
     *
     * @return object
     */
    public function decodeOrFail()
    {
        if (!$document = $this->decode()) {
            throw new DocumentRequiredException();
        }

        return $document;
    }

    /**
     * Get the domain record type that is subject of the request.
     *
     * @return string
     */
    public function getType()
    {
        return $this->route->getType();
    }

    /**
     * Get the resource type that the request is for.
     *
     * @return string|null
     */
    public function getResourceType()
    {
        return $this->route->getResourceType();
    }

    /**
     * Get the validated JSON API document, if there is one.
     *
     * @return DocumentInterface|null
     * @deprecated 2.0.0
     */
    public function getDocument()
    {
        if (!$document = $this->decode()) {
            return null;
        }

        return new Document($document);
    }

    /**
     * Get the JSON API encoding parameters.
     *
     * @return EncodingParametersInterface
     * @deprecated 2.0.0 use `getEncodingParameters`
     */
    public function getParameters()
    {
        return $this->getEncodingParameters();
    }

    /**
     * @return EncodingParametersInterface
     */
    public function getEncodingParameters()
    {
        if ($this->parameters) {
            return $this->parameters;
        }

        $parser = $this->factory->createQueryParametersParser();

        return $this->parameters = $parser->parseQueryParameters(
            $this->request->query()
        );
    }

    /**
     * Validate the JSON API request.
     *
     * This method maintains compatibility with Laravel 5.5, as the `ValidatesWhenResolved`
     * method was renamed to `validateResolved` in 5.6.
     *
     * @return void
     * @todo remove when dropping support for Laravel 5.5.
     */
    public function validate()
    {
        $this->validateResolved();
    }

    /**
     * @inheritdoc
     */
    public function validateResolved()
    {
        $this->authorize();
        $this->validateQuery();
        $this->validateDocument();
    }

    /**
     * @return Route
     */
    protected function getRoute(): Route
    {
        return $this->route;
    }

    /**
     * Validate the JSON API document.
     *
     * @return void
     * @throws JsonApiException
     */
    protected function validateDocument()
    {
        // no-op
    }

    /**
     * Run the validation and throw an exception if it fails.
     *
     * @param DocumentValidatorInterface|ValidatorInterface $validator
     * @throws ValidationException
     */
    protected function passes($validator)
    {
        if ($validator->fails()) {
            $this->failedValidation($validator);
        }
    }

    /**
     * @param DocumentValidatorInterface|ValidatorInterface $validator
     * @throws ValidationException
     */
    protected function failedValidation($validator)
    {
        if ($validator instanceof ValidatorInterface) {
            throw ValidationException::create($validator);
        }

        throw new ValidationException($validator->getErrors());
    }

    /**
     * @return AuthorizerInterface|null
     */
    protected function getAuthorizer()
    {
        return $this->container->getAuthorizerByResourceType($this->getResourceType());
    }

    /**
     * Get the resource validators.
     *
     * @return ValidatorFactoryInterface|ValidatorProviderInterface|null
     */
    protected function getValidators()
    {
        return $this->container->getValidatorsByResourceType($this->getResourceType());
    }

    /**
     * Get the inverse resource validators.
     *
     * @return ValidatorFactoryInterface|ValidatorProviderInterface|null
     */
    protected function getInverseValidators()
    {
        return $this->container->getValidatorsByResourceType(
            $this->route->getInverseResourceType()
        );
    }

}


<?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\Headers;

use CloudCreativity\LaravelJsonApi\Contracts\Document\MutableErrorInterface;
use CloudCreativity\LaravelJsonApi\Contracts\Repositories\ErrorRepositoryInterface;
use Neomerx\JsonApi\Contracts\Codec\CodecMatcherInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeaderParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\HeadersCheckerInterface;
use Neomerx\JsonApi\Exceptions\JsonApiException as E;

class RestrictiveHeadersChecker implements HeadersCheckerInterface
{

    const NOT_ACCEPTABLE = 'headers:not-acceptable';
    const MULTIPLE_MEDIA_TYPES = 'headers:multiple-media-types';
    const UNSUPPORTED_MEDIA_TYPE = 'headers:unsupported-media-type';

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

    /**
     * @var ErrorRepositoryInterface
     */
    private $errors;

    /**
     * RestrictiveHeadersChecker constructor.
     *
     * @param CodecMatcherInterface $codecMatcher
     * @param ErrorRepositoryInterface $errors
     */
    public function __construct(CodecMatcherInterface $codecMatcher, ErrorRepositoryInterface $errors)
    {
        $this->codecMatcher = $codecMatcher;
        $this->errors = $errors;
    }

    /**
     * @param HeaderParametersInterface $parameters
     *
     * @return void
     */
    public function checkHeaders(HeaderParametersInterface $parameters)
    {
        // Note: for these checks the order is specified by spec. See details inside.

        $this->checkAcceptHeader($parameters);

        // clients must send content type only if the request has body so there is no point in checking
        // `Content-Type` header if the original request is bodiless.
        if ($parameters->getContentTypeHeader() !== null) {
            $this->checkContentTypeHeader($parameters);
        }
    }

    /**
     * @param HeaderParametersInterface $parameters
     *
     * @return void
     */
    protected function checkAcceptHeader(HeaderParametersInterface $parameters)
    {
        $this->codecMatcher->matchEncoder($parameters->getAcceptHeader());

        // From spec: Servers MUST respond with a 406 Not Acceptable status code
        // if a request's Accept header contains the JSON API media type and all
        // instances of that media type are modified with media type parameters.

        // We return 406 if no match found for encoder (media type with or wo parameters)
        // If no encoders were configured for media types with parameters we return 406 anyway
        if ($this->codecMatcher->getEncoderHeaderMatchedType() === null) {
            throw new E([$this->createNotAcceptableError()], E::HTTP_CODE_NOT_ACCEPTABLE);
        }
    }

    /**
     * @param HeaderParametersInterface $parameters
     *
     * @return void
     */
    protected function checkContentTypeHeader(HeaderParametersInterface $parameters)
    {
        // Do not allow specify more than 1 media type for input data. Otherwise which one is correct?
        if (count($parameters->getContentTypeHeader()->getMediaTypes()) > 1) {
            throw new E([$this->createMultipleMediaTypesError()], E::HTTP_CODE_BAD_REQUEST);
        }

        $this->codecMatcher->matchDecoder($parameters->getContentTypeHeader());

        // From spec: Servers MUST respond with a 415 Unsupported Media Type status code
        // if a request specifies the header Content-Type: application/vnd.api+json with
        // any media type parameters.

        // We return 415 if no match found for decoder (media type with or wo parameters)
        // If no decoders were configured for media types with parameters we return 415 anyway
        if ($this->codecMatcher->getDecoderHeaderMatchedType() === null) {
            throw new E([$this->createUnsupportedMediaTypeError()], E::HTTP_CODE_UNSUPPORTED_MEDIA_TYPE);
        }
    }

    /**
     * @return MutableErrorInterface
     */
    protected function createNotAcceptableError()
    {
        return $this->errors->error(self::NOT_ACCEPTABLE);
    }

    /**
     * @return MutableErrorInterface
     */
    protected function createMultipleMediaTypesError()
    {
        return $this->errors->error(self::MULTIPLE_MEDIA_TYPES);
    }

    /**
     * @return MutableErrorInterface
     */
    protected function createUnsupportedMediaTypeError()
    {
        return $this->errors->error(self::UNSUPPORTED_MEDIA_TYPE);
    }
}


<?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\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Collection;

/**
 * Class MakeResource
 *
 * @package CloudCreativity\LaravelJsonApi
 */
class MakeResource extends Command
{

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = "make:json-api:resource
        {resource : The resource to create files for.}
        {api? : The API that the resource belongs to.}
        {--a|auth : Generate a resource authorizer.}
        {--c|content-negotiator : Generate a resource content negotiator.}
        {--e|eloquent : Use Eloquent classes.}
        {--N|no-eloquent : Do not use Eloquent classes.}
        {--o|only= : Specify the classes to generate.}
        {--x|except= : Skip the specified classes.}
    ";

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a full JSON API resource.';

    /**
     * The available generator commands.
     *
     * @var array
     */
    private $commands = [
        'make:json-api:adapter' => MakeAdapter::class,
        'make:json-api:schema' => MakeSchema::class,
        'make:json-api:validators' => MakeValidators::class,
    ];

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $resourceParameter = [
            'resource' => $this->argument('resource'),
            'api' => $this->argument('api'),
        ];

        $eloquentParameters = array_merge($resourceParameter, [
            '--eloquent' => $this->option('eloquent'),
            '--no-eloquent' => $this->option('no-eloquent'),
        ]);

        $commands = collect($this->commands);

        /** Just tell the user, if no files are created */
        if ($commands->isEmpty()) {
            $this->info('No files created.');
            return 0;
        }

        /** Filter out any commands the user asked os to. */
        if ($this->option('only') || $this->option('except')) {
            $type = $this->option('only') ? 'only' : 'except';

            $commands = $this->filterCommands($commands, $type);
        }

        /** Run commands that cannot accept Eloquent parameters. */
        $notEloquent = ['make:json-api:validators'];

        if (!$this->runCommandsWithParameters($commands->only($notEloquent), $resourceParameter)) {
            return 1;
        }

        /** Run commands that can accept Eloquent parameters. */
        $eloquent = ['make:json-api:adapter', 'make:json-api:schema'];

        if (!$this->runCommandsWithParameters($commands->only($eloquent), $eloquentParameters)) {
            return 1;
        }

        /** Authorizer */
        if ($this->option('auth')) {
            $this->call('make:json-api:authorizer', [
                'name' => $this->argument('resource'),
                'api' => $this->argument('api'),
                '--resource' => true,
            ]);
        }

        /** Content Negotiator */
        if ($this->option('content-negotiator')) {
            $this->call('make:json-api:content-negotiator', [
                'name' => $this->argument('resource'),
                'api' => $this->argument('api'),
                '--resource' => true,
            ]);
        }

        /** Give the user a digial high-five. */
        $this->comment('All done, keep doing what you do.');

        return 0;
    }

    /**
     * Filters out commands using either 'except' or 'only' filter.
     *
     * @param Collection $commands
     * @param string $type
     * @return Collection
     */
    private function filterCommands(Collection $commands, $type)
    {
        $baseCommandName = 'make:json-api:';
        $filterValues = explode(',', $this->option($type));

        $targetCommands = collect($filterValues)
            ->map(function ($target) use ($baseCommandName) {
                return $baseCommandName . strtolower(trim($target));
            });

        return $commands->{$type}($targetCommands->toArray());
    }

    /**
     * Runs the given commands and passes them all the given parameters.
     *
     * @param Collection $commands
     * @param array $parameters
     * @return bool
     */
    private function runCommandsWithParameters(Collection $commands, array $parameters)
    {
        foreach ($commands->keys() as $command) {
            if (0 !== $this->call($command, $parameters)) {
                return false;
            }
        }

        return true;
    }

}