cloudcreativity / laravel-json-api

JSON API (jsonapi.org) package for Laravel applications.
http://laravel-json-api.readthedocs.io/en/latest/
Apache License 2.0
780 stars 109 forks source link

Resolving Adapter #444

Closed luceos closed 5 years ago

luceos commented 5 years ago

First off, 🙇 what a great package ❤️ !

A lot of hard work has gone into this, exceptionally well done!


I am refactoring a pretty large app by using Domains (DDD), so my Laravel app has a component directory containing some items like User and Extension. To structure the API related code inside of that I gave each of them a Api subdirectory with a ResourceProvider registered inside the json-api-v1.php configuration. In order to support this structure I created a App\JsonApi\Resolver class, implemented the resolve method and configured it inside the json-api-v1.php.

Now when trying to hit the resource route for extensions I receive an exception:

No adapter for resource type: extensions

Thrown by vendor/cloudcreativity/laravel-json-api/src/Store/Store.php:341.

I was assuming that my Resolver would be used everywhere, but dd'ing inside of it doesn't make a difference. When I dd inside the CloudCreativity\LaravelJsonApi\Factories\Factory::createResolver method I actually see my Resolver is being used, but no resources have been registered.

lindyhopchris commented 5 years ago

Thanks!

configured it inside the json-api-v1.php.

Can you provide your configuration?

luceos commented 5 years ago
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Resolver
    |--------------------------------------------------------------------------
    |
    | The API's resolver is the class that works out the fully qualified
    | class name of adapters, schemas, authorizers and validators for your
    | resource types. We recommend using our default implementation but you
    | can override it here if desired.
    */
    'resolver' => \App\JsonApi\Resolver::class,

    /*
    |--------------------------------------------------------------------------
    | Root Namespace
    |--------------------------------------------------------------------------
    |
    | The root namespace for JSON API classes for this API. If `null`, the
    | namespace will default to `JsonApi` within your application's root
    | namespace (obtained via Laravel's `Application::getNamespace()`
    | method).
    |
    | The `by-resource` setting determines how your units are organised within
    | your root namespace.
    |
    | - true:
    |   - e.g. App\JsonApi\Posts\{Adapter, Schema, Validators}
    |   - e.g. App\JsonApi\Comments\{Adapter, Schema, Validators}
    | - false:
    |   - e.g. App\JsonApi\Adapters\PostAdapter, CommentAdapter}
    |   - e.g. App\JsonApi\Schemas\{PostSchema, CommentSchema}
    |   - e.g. App\JsonApi\Validators\{PostValidator, CommentValidator}
    |
    */
    'namespace' => '',
    'by-resource' => false,

    /*
    |--------------------------------------------------------------------------
    | Resources
    |--------------------------------------------------------------------------
    |
    | Here you map the list of JSON API resources in your API to the actual
    | record (model/entity) classes they relate to.
    |
    | For example, if you had a `posts` JSON API resource, that related to
    | an Eloquent model `App\Post`, your mapping would be:
    |
    | `'posts' => App\Post::class`
    */
    'resources' => [
    ],

    /*
    |--------------------------------------------------------------------------
    | Eloquent
    |--------------------------------------------------------------------------
    |
    | Whether your JSON API resources predominantly relate to Eloquent models.
    | This is used by the package's generators.
    |
    | You can override the setting here when running a generator. If the
    | setting here is `true` running a generator with `--no-eloquent` will
    | override it; if the setting is `false`, then `--eloquent` is the override.
    |
    */
    'use-eloquent' => true,

    /*
    |--------------------------------------------------------------------------
    | URL
    |--------------------------------------------------------------------------
    |
    | The API's url, made up of a host, URL namespace and route name prefix.
    |
    | If a JSON API is handling an inbound request, the host will always be
    | detected from the inbound HTTP request. In other circumstances
    | (e.g. broadcasting), the host will be taken from the setting here.
    | If it is `null`, the `app.url` config setting is used as the default.
    | If you set `host` to `false`, the host will never be appended to URLs
    | for inbound requests.
    |
    | The name setting is the prefix for route names within this API.
    |
    */
    'url' => [
        'host' => null,
        'namespace' => '/api/v1',
        'name' => 'api:v1:',
    ],

    /*
    |--------------------------------------------------------------------------
    | Controllers
    |--------------------------------------------------------------------------
    |
    | The default JSON API controller wraps write operations in transactions.
    | You can customise the connection for the transaction here. Or if you
    | want to turn transactions off, set `transactions` to `false`.
    |
    */
    'controllers' => [
        'transactions' => true,
        'connection' => null,
    ],

    /*
    |--------------------------------------------------------------------------
    | Jobs
    |--------------------------------------------------------------------------
    |
    | Defines settings for the asynchronous processing feature. We recommend
    | referring to the documentation on asynchronous processing if you are
    | using this feature.
    |
    | Note that if you use a different model class, it must implement the
    | asynchronous process interface.
    |
    */
    'jobs' => [
        'resource' => 'queue-jobs',
        'model' => \CloudCreativity\LaravelJsonApi\Queue\ClientJob::class,
    ],

    /*
    |--------------------------------------------------------------------------
    | Supported JSON API Extensions
    |--------------------------------------------------------------------------
    |
    | Refer to the JSON API spec for information on supported extensions.
    |
    */
    'supported-ext' => null,

    /*
    |--------------------------------------------------------------------------
    | Encoding Media Types
    |--------------------------------------------------------------------------
    |
    | This defines the JSON API encoding used for particular media
    | types supported by your API. This array can contain either
    | media types as values, or can be keyed by a media type with the value
    | being the options that are passed to the `json_encode` method.
    |
    | These values are also used for Content Negotiation. If a client requests
    | via the HTTP Accept header a media type that is not listed here,
    | a 406 Not Acceptable response will be sent.
    |
    | If you want to support media types that do not return responses with JSON
    | API encoded data, you can do this at runtime. Refer to the
    | Content Negotiation chapter in the docs for details.
    |
    */
    'encoding' => [
        'application/vnd.api+json',
    ],

    /*
    |--------------------------------------------------------------------------
    | Decoding Media Types
    |--------------------------------------------------------------------------
    |
    | This defines the media types that your API can receive from clients.
    | This array is keyed by expected media types, with the value being the
    | service binding that decodes the media type.
    |
    | These values are also used for Content Negotiation. If a client sends
    | a content type not listed here, it will receive a
    | 415 Unsupported Media Type response.
    |
    | Decoders can also be calculated at runtime, and/or you can add support
    | for media types for specific resources or requests. Refer to the
    | Content Negotiation chapter in the docs for details.
    |
    */
    'decoding' => [
        'application/vnd.api+json',
    ],

    /*
    |--------------------------------------------------------------------------
    | Providers
    |--------------------------------------------------------------------------
    |
    | Providers allow vendor packages to include resources in your API. E.g.
    | a Shopping Cart vendor package might define the `orders` and `payments`
    | JSON API resources.
    |
    | A package author will define a provider class in their package that you
    | can add here. E.g. for our shopping cart example, the provider could be
    | `Vendor\ShoppingCart\JsonApi\ResourceProvider`.
    |
    */
    'providers' => [
        Component\Extension\Api\ResourceProvider::class,
        Component\User\Api\ResourceProvider::class,
    ],
];
lindyhopchris commented 5 years ago

The resource providers have a getResolver() method on them - you might need to overload that and return your own instance.

The resolver that's used from the resolver key of your config is the resolver for the things in your config... so won't have any affect on the resource providers. The resource providers basically do their own thing - so that they are operationally independent (they're like packages).

So I think if you overload the getResolver() method on your providers to use your custom resolver you should be ok, but let me know? https://github.com/cloudcreativity/laravel-json-api/blob/develop/src/Api/AbstractProvider.php#L62-L68

luceos commented 5 years ago

So.. that was a great pointer, but did not completely fix it. In addition to that I also had to ensure that the Resolver::isResourceType would return true for the supported resources (in my case all of them).

Thanks for your help!

luceos commented 5 years ago

One more question, if you allow me.

The resolving now works, but when it tries to instantiate an adapter it will use the construct argument from the AbstractAdapter, which is an Eloquent Model and cannot be instantiated:

[2019-10-23 12:41:49] local.ERROR: Target [Illuminate\Database\Eloquent\Model] is not instantiable while building [Component\Extension\Api\Adapters\ExtensionAdapter]. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target [Illuminate\\Database\\Eloquent\\Model] is not instantiable while building [Component\\Extension\\Api\\Adapters\\ExtensionAdapter]. at vendor/laravel/framework/src/Illuminate/Container/Container.php:979)
[stacktrace]
#0 vendor/laravel/framework/src/Illuminate/Container/Container.php(813): Illuminate\\Container\\Container->notInstantiable('Illuminate\\\\Data...')

I was considering doing a conditional ioc binding ($this->app->when(ExtensionAdapter)->needs()->give()), but shouldn't there be a better way?


Edit: I've actually overridden the construct and set the correct model as argument, is that the recommended way?

lindyhopchris commented 5 years ago

The recommended way is to type-hint the actual model the adapter relates to in the constructor, then pass that into the parent constructor, eg in the generated Eloquent adapter: https://github.com/cloudcreativity/laravel-json-api/blob/develop/stubs/eloquent/adapter.stub#L32-L35

If your child adapter doesn't relate to an Eloquent model, then you shouldn't be extending the Eloquent adapter.

Hope that helps!

luceos commented 5 years ago

Okay thanks you, I am now generating an index with valid json api spec. Thanks a bunch!