Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Model to JsonApi object in a custom Controller. #36

Closed
chrisngabp opened this issue Jan 4, 2017 · 23 comments
Closed

Convert Model to JsonApi object in a custom Controller. #36

chrisngabp opened this issue Jan 4, 2017 · 23 comments
Milestone

Comments

@chrisngabp
Copy link

Hello, I'd like to know if the package has any way to convert an object model to a json with the jsonapi structure in any controller (not necessarily an Eloquent jsonapi controller).

Thank you!

@lindyhopchris
Copy link
Member

Hi!

Yes, extend the JsonApiController rather than the EloquentController. Then do something similar to what the EloquentController does in its methods, e.g the read method:
https://github.com/cloudcreativity/laravel-json-api/blob/master/src/Http/Controllers/EloquentController.php#L132

Note that you'll need to attach an adapter to so that all the parsing of the incoming request knows how to check whether a resource id is valid and can load the record that the id relates to. You need to extend the AdapterInterface and then list the full qualified class name in your json-api config file here:
https://github.com/cloudcreativity/laravel-json-api/blob/master/config/json-api.php#L130-L144

For an example of how this is done for Eloquent models, see this:
https://github.com/cloudcreativity/laravel-json-api/blob/master/src/Adapters/EloquentAdapter.php

I appreciate I really need to write some documentation... in the meantime feel free to ask any questions here.

@chrisngabp
Copy link
Author

chrisngabp commented Jan 5, 2017

Hi lindyhopchris! thank you very much for your response!
I'll try to explain better what I need to do. My question was more like "how can I encode a Model to a JsonApi Object to print it in some view".
The example of what i'm trying to do: I want to have an encoded model printed in a view to use it as a preloaded object without having to make a request to the api.

EDIT: I have found a way of doing what I want using neomerx encoder like this:

`
use Neomerx\JsonApi\Encoder\Encoder;

(...)

$myModel = new MyModel();
Encoder::instance([
MyModel::class => MyModelSchema::class
])->encodeData($myModel);

`

Does laravel-json-api have a provider or a way to resolve something like I resolved with neomerx in the example??

@lindyhopchris
Copy link
Member

Hi! Yes, that's the solution for the moment - manually create an encoder and then encode the data yourself.

I'm thinking that it would be really useful to have an approach (including a Blade directive) for outputting JSON API encoded data into a view. The use case you describe would be quite common in my opinion.

I'm going to leave this issue open as it's effectively a feature request!

@lindyhopchris lindyhopchris added this to the 0.6.0 milestone Jan 9, 2017
@chrisngabp
Copy link
Author

Thank you very much! It'll be great to have something like that in a future.

Another thing i've been trying to do and could be related to this in some way is to find a way to throw an exception or instance an error using the laravel-json-api errors.
Example: My controller (not a jsonapi one) receives a file via request and the file is not valid. Then I want to do something like "return RESOURCE_INVALID_ATTRIBUTES_MESSAGES".

Reference: https://github.com/cloudcreativity/laravel-json-api/blob/master/config/json-api-errors.php#L67

If you consider I can open another issue for this case :)

@lindyhopchris
Copy link
Member

If you're using the ReplyTrait on your controller, you can return a string for the error. The string is the key that is defined in your JSON API errors config file.

So you can do the following on your controller:

return $this->reply()->error('my_error_key');

@GregPeden
Copy link
Contributor

Just a +1 vote for this, I came here looking for a way to "consume" the API within the Laravel code base.

In my case, my web app is consuming its own JSON API for AJAX actions, and also I am using Laravel Echo to broadcast events. My app will have multiple people collaborating in the same space on separate devices, so other users need model events (create/edit/delete) broadcast to them. It would be swell for the broadcast JSON structure to follow the same format as the API does, because then I can use the same client-side code to process both cases (API response content and event broadcast content). Using your API package to do this ensures that in both cases the JSON structure would be identical even through design changes, which will make developmental do-it-by-myself testing more reliable.

@lindyhopchris
Copy link
Member

Broadcast is definitely an aim. We're using broadcast at the moment but having to do it manually as follows:

    /**
     * Get the data to broadcast.
     *
     * @return array
     */
    public function broadcastWith()
    {
        /** @var CloudCreativity\JsonApi\Encoder\Encoder $encoder */
        $encoder = Encoder::instance([
            Post::class => Posts\Schema::class,
            Comment::class => Comments\Schema::class,
        ]);

        return $encoder->serializeData($this->post);
    }

Obviously what would be nicer is to have a more fluent way to do this, and get the encoder already loaded with the schema definitions that you set in your config.

If you are using the above as a temporary solution then remember to include in the array all the classes/schemas that could appear in the "tree" of resources that you are encoding.

@GregPeden
Copy link
Contributor

That is really helpful, thanks!

@GregPeden
Copy link
Contributor

GregPeden commented Apr 22, 2017

Hello again.

I would like the Broadcast response to include the attributes within some of the relationships. For the life of me I cannot figure out how to make the attributes include the attributes on relationships. Any tips?

I have tried a bunch of stuff but here is how I have it set up at the moment.

    public function broadcastWith()
    {
        Log::info('Device event:', ['class' => self::class, 'data' => $this->device]);

        $this->device->load('building', 'type');
        return $encoder = Encoder::instance([
            Device::class => Devices\Schema::class,
            Building::class => Buildings\Schema::class,
            DeviceType::class => DeviceTypes\Schema::class
        ])->serializeData($this->device);
    }

In its schema:

    public function getRelationships($resource, $isPrimary, array $includeRelationships)
    {
        if (!$resource instanceof Device) {
            throw new RuntimeException('Expecting a Device model.');
        }
        return [
            'building' => [
                self::SHOW_SELF => true,
                self::SHOW_RELATED => true,
                self::DATA => $resource->building,
            ],
            'device-type' => [
                self::SHOW_SELF => true,
                self::SHOW_RELATED => true,
                self::DATA => isset($includeRelationships['device-type']) ?
                    $resource->type : $this->createBelongsToIdentity($resource, 'type'),
            ]
        ];
    }

whether or not I include "self::SHOW_DATA => true" above doesn't seem to affect the response when encoded in this way.

High-level this works, except the response is the top-level attributes on the Device object, and the attributes for Building and DeviceType are missing.

The log event is there just to prove that the Building model has the relationships loaded, even though I suspect the real intended method is to tell it to include the relationship by some other means.

[2017-04-22 05:18:20] local.INFO: Device event: {"class":"App\\Events\\Broadcasts\\Device\\DeviceEvent","data":"[object] (App\\Models\\Building\\Device: {\"id\":49,\"building_id\":2,\"device_type_id\":1,\"designation\":\"31\",\"slug\":\"31\",\"group\":\"Test\",\"licence\":null,\"created_at\":\"2017-04-22 05:18:19\",\"updated_at\":\"2017-04-22 05:18:19\",\"building\":{\"id\":2,\"name\":\"Fake Building\"}})"} 

I am aware that the second parameter in serializeData() is a EncodingParametersInterface object but I cannot find documentation on this nor can I tell how this is meant to be used by reading through the code.

@lindyhopchris
Copy link
Member

Hello! You'll need to include the related resources. So you'll need to pass the encoder a set of encoding parameters, which with the include parameters as building and device-type.

The actual relationships in the JSON API spec don't include attributes of the related resources - as a relationship is a resource identifier, not the resource itself. That's why the included key contains a list of related resources that have been asked for.

@GregPeden
Copy link
Contributor

Awesome, thanks!

For those who look this up later, the solution is as follows:

return Encoder::instance([
    Device::class => Devices\Schema::class,
    Building::class => Buildings\Schema::class,
    DeviceType::class => DeviceTypes\Schema::class
])->serializeData($this->device, new EncodingParameters(['building', 'device-type']));

@lindyhopchris
Copy link
Member

Great, glad that helped. I'm planning on adding a trait that can be applied to make broadcasting a lot easier. Haven't done it yet because in all the apps we use the package on, we're not doing any broadcasting yet (for legacy reasons), but we're planning to start adding in broadcasting soon.

@lindyhopchris lindyhopchris mentioned this issue May 18, 2017
15 tasks
@GregPeden
Copy link
Contributor

Oh hello... so, concerning version 0.8 and its config file changes, can you suggest a way to generate a schema relationships array in a way which produces the "schemas.defaults" config array used in version 0.7?

The context is that I made my own hacky "getFromApi" helper function for use in broadcasters and initial state generation, and it was reading the config file to get all of the schema mappings so that I didn't have to worry about feeding it the schema relationships in every space.

It looked like this:

    /**
     * @param Eloquent|Collection|array $models
     * @param string|array $includes
     * @return mixed
     */
    public function apiEncoder($models, $includes = []) {
        if (is_string($includes)) {
            $includes = [$includes];
        }

        return Encoder::instance(Config::get('json-api.schemas.defaults'))->serializeData($models, new EncodingParameters($includes));
    }

As of 0.8 that doesn't work, here is a even hackier example of how one could fix it:

    /**
     * @param Eloquent|Collection|array $models
     * @param string|array $includes
     * @return mixed
     */
    public function apiEncoder($models, $includes = []) {
        if (is_string($includes)) {
            $includes = [$includes];
        }

        $schemas = [
            Models\Address\Address::class => JsonApi\Addresses\Schema::class,
            Models\Building\Building::class => JsonApi\Buildings\Schema::class,
            Models\Address\City::class => JsonApi\Cities\Schema::class,
            Models\Client\ClientCompany::class => JsonApi\ClientCompanies\Schema::class,
            Models\Contractor\ContractorCompany::class => JsonApi\ContractorCompanies\Schema::class,
            Models\Address\Country::class => JsonApi\Countries\Schema::class,
            Models\Address\CountryDivision::class => JsonApi\CountryDivisions\Schema::class,
            Models\Building\Device::class => JsonApi\Devices\Schema::class,
            Models\Building\DeviceType::class => JsonApi\DeviceTypes\Schema::class,
            Models\Office\Office::class => JsonApi\Offices\Schema::class,
            Models\Project\ProjectStatus::class => JsonApi\ProjectStatuses\Schema::class,
            Models\Project\ProjectType::class => JsonApi\ProjectTypes\Schema::class,
            Models\Project\Project::class => JsonApi\Projects\Schema::class,
            Models\Task\Task::class => JsonApi\Tasks\Schema::class,
            Models\Task\TaskType::class => JsonApi\TaskTypes\Schema::class,
        ];

        return Encoder::instance($schemas)->serializeData($models, new EncodingParameters($includes));
    }

In the short-term, I am going to basically include this 'schemas' list in the config file, which is off-spec per your current designs. I figure that there might be a "getSchemas()" method somewhere but I cannot find it. If I use the JsonApi Facade, ie JsonApi::getApi()->getSchemas(), it complains that the API is not initialized.

@lindyhopchris
Copy link
Member

lindyhopchris commented May 24, 2017

Hi! Yes, had forgotten you were doing that. Here's how to get the array:

$repository = app(CloudCreativity\LaravelJsonApi\Api\Repository::class);
$schemasArray = $repository->retrieveApi('api-name')->getResources()->getSchemas();

It's a bit convoluted at the moment but I will be making it simpler in the near future (it's on the 1.0 to-do list).

@GregPeden
Copy link
Contributor

Awesome, thanks! For now, convoluted is fine, I understand that you intend to improve on the handling of this aspect so for now I am just using a helper and figure I can sub in your official solution whenever it is ready.

@lindyhopchris
Copy link
Member

Great! That code snippet actually won't change, it's just I'll hide it behind an "official" method to get an encoder by API name, which is effectively what you're currently doing that the mo.

@lindyhopchris
Copy link
Member

lindyhopchris commented May 24, 2017

Sorry - slight typo in the snippet above, the namespace should be CloudCreativity\LaravelJsonApi\Api\Repository (just edited the snippet above in case anyone else looks at it)

@GregPeden
Copy link
Contributor

GregPeden commented May 24, 2017

Unfortunately I am running in to some errors.

First, I assume that given the config file "json-api-default.php", the intended API name is 'default'.

Running the code as suggested (with the namespace fix), I get 'call to undefined method CloudCreativity\LaravelJsonApi\Schema\Container::getResources()'

The IDE hints that I can call "getSchemas()" directly, as so:

$repository = app(Repository::class);
$schemasArray = $repository->retrieveApi('default')->getSchemas();

This returns a container, not an array. One of the members is a property "-providerMapping", which includes the hyphen in the name, and I cannot figure out how to access it, however it contains the required array structure for the encoder.

@lindyhopchris
Copy link
Member

Really sorry, gave you the wrong method to invoke. Use $repository->retrieveDefinition('default'). The definition is a class representing the config file.

@lindyhopchris
Copy link
Member

So $repository->retrieveDefinition('default')->getResources()->getSchemas()

@GregPeden
Copy link
Contributor

Thank you, this works as expected.

@GregPeden
Copy link
Contributor

For those who visit this in the future, here is the helper method I am using:

use CloudCreativity\LaravelJsonApi\Api\Repository;
use CloudCreativity\JsonApi\Encoder\Encoder;
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;
...
    /**
     * @param Eloquent|Collection|array $models
     * @param string|array $includes
     * @return mixed
     */
    public function apiEncoder($models, $includes = []) {
        if (is_string($includes)) {
            $includes = [$includes];
        }

        return Encoder::instance(
            app(Repository::class)->retrieveDefinition('default')->getResources()->getSchemas()
        )->serializeData($models, new EncodingParameters($includes));
    }

lindyhopchris added a commit that referenced this issue Jun 2, 2017
An encoder can now be obtained for a named API using the
JSON API service's `encoder` method.

This contributes towards Issue #36
@lindyhopchris
Copy link
Member

The following are on the develop branch and will be released as v0.9. I'll add chapters to the wiki when I release that version:

Broadcasting

Can now use the Broadcasting\BroadcastsData trait. E.g.:

public function broadcastWith()
{
  // $includes is optional, can be a string or array of strings
  return $this->serializeData($this->user, $includes);
}

This will use the default API schema set. To change the API to use, set the $broadcastApi property on the object that the trait is applied to. If you need to work out the API at runtime, you can overload the broadcastApi method.

Blade Templates

In a blade template, you can now do @encode($data, $includes) - $includes is optional as per above. This will output the data in the template. Example usage would be:

<script type="application/vnd.api+json">@encode($posts)</script>

This will use the default API. To use a different one, or to set encoding options, use the @jsonapi directive before @encoder. For example:

@jsonapi('v1', null, JSON_PRETTY_PRINT);
<script type="application/vnd.api+json">
  @encode($posts)
</script>

You only need to use @jsonapi once, even if using multiple @encode calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants