neomerx / json-api

Framework agnostic JSON API (jsonapi.org) implementation
Apache License 2.0
740 stars 65 forks source link

Example of \Neomerx\JsonApi\Http\Responses use #182

Closed pablorsk closed 7 years ago

pablorsk commented 7 years ago

I'm triying use \Neomerx\JsonApi\Http\Responses on Laravel + PSR-7, but I can't obtain an instance of $responses.

My code (all together only of explanation porporsues):

use App\Neomerx\Models\Author;
use App\Neomerx\Models\Photo;
use App\Neomerx\Schemas\AuthorSchema;
use App\Neomerx\Schemas\PhotoSchema;
use Neomerx\JsonApi\Encoder\Encoder;
use Neomerx\JsonApi\Encoder\EncoderOptions;
use Psr\Http\Message\ServerRequestInterface;

Route::get('/example/authors', function (ServerRequestInterface $request) {
    $data = new Author();
    $objects = $data->paginate()->toArrayObjects(); // collection of Author objects

    $encoderArray = [
        Author::class => AuthorSchema::class,
        Photo::class => PhotoSchema::class,
    ];

    $url = '/example/books/';
    $encoder = Encoder::instance($encoderArray, new EncoderOptions(JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES, $url));
    $result = $encoder->encodeData($objects);

    return $result;

    // $response = ?????;
    // return $response->getContentResponse($objects);
});

More information on the wiki, but only says We can extend, but not found any practical example.

How can I set $response if I have PSR-7 $request?

neomerx commented 7 years ago

Short answer: for simple GET responses you can use plain Response with $result as body and proper Content-Type (application/vnd.api+json).

Deeper answer: encoding objects into JSON API has some specifics such as certain headers should be added in certain cases thus ResponsesInterface implementation should wrap Encoder itself. So its usage looks more like

$response->getContentResponse($objects);

but not

$response->getContentResponse($encoder->encodeData($objects));

Working practical example is here and ResponsesInterface implementation is here (based on Zend\Diactoros\Response PSR-7).

neomerx commented 7 years ago

@pablorsk As the lib author can you give a working sample of client app? Preferably with a free license.

neomerx commented 7 years ago

@pablorsk I haven't received any feedback from you for some time and have a feeling that your question was answered. If you have any further questions please don't hesitate to contact.

pablorsk commented 7 years ago

@neomerx,

About ts-angular-jsonapi, It would be an honor to make an example that works with neomerx json-api, but I need an online example, is it possible?

About Responses, first of all, THANK YOU. But, when you make an instance of Resposes can't find how make some params:

Do you have an example?

For now, with my example+your links...

Route::get('/example/authors', function (ServerRequestInterface $request) {
    $data = new Author();
    $objects = $data->paginate()->toArrayObjects(); // collection of Author objects

    $encoderArray = [
        Author::class => AuthorSchema::class,
        Photo::class => PhotoSchema::class,
    ];

    $url = '/example/books/';
    $encoder = Encoder::instance($encoderArray, new EncoderOptions(JSON_PRETTY_PRINT, $url));
    /*
    // old way
    $result = $encoder->encodeData($objects);
    return $result;
     */

    $outputMediaType = ???; //MediaTypeInterface
    $extensions = new Neomerx\JsonApi\Http\Headers\SupportedExtensions();
    $schemes = ???;    // ContainerInterface

    $responses = new Responses(
        $outputMediaType,
        $extensions,
        $encoder,
        $schemes
     );

    return $responses->getContentResponse($objects);
});
neomerx commented 7 years ago

@pablorsk Do you mean an online example of a server? Sure. If you have PHP and composer installed in less than 60 seconds.

$ composer create-project --prefer-dist limoncello-php/app app_name
$ cd app_name
$ composer l:db migrate 
$ composer l:db seed
$ php -S 0.0.0.0:8080 -t public/ public/index.php

Done. Now you have a working demo server on port 8080

Basic test to check

$ curl -X GET http://localhost:8080/api/v1/boards -H 'accept: application/vnd.api+json'

will output

{"data":[{"type":"boards","id":"1","attributes":{"title":"Board Rerum sit atque.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/1/relationships/posts","related":"/api/v1/boards/1/posts"}}},"links":{"self":"/api/v1/boards/1"}},{"type":"boards","id":"2","attributes":{"title":"Board Dignissimos.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/2/relationships/posts","related":"/api/v1/boards/2/posts"}}},"links":{"self":"/api/v1/boards/2"}},{"type":"boards","id":"3","attributes":{"title":"Board In qui cumque.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/3/relationships/posts","related":"/api/v1/boards/3/posts"}}},"links":{"self":"/api/v1/boards/3"}},{"type":"boards","id":"4","attributes":{"title":"Board Quaerat magni.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/4/relationships/posts","related":"/api/v1/boards/4/posts"}}},"links":{"self":"/api/v1/boards/4"}},{"type":"boards","id":"5","attributes":{"title":"Board Deserunt et.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/5/relationships/posts","related":"/api/v1/boards/5/posts"}}},"links":{"self":"/api/v1/boards/5"}},{"type":"boards","id":"6","attributes":{"title":"Board Odit ducimus vitae.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/6/relationships/posts","related":"/api/v1/boards/6/posts"}}},"links":{"self":"/api/v1/boards/6"}},{"type":"boards","id":"7","attributes":{"title":"Board Aut qui cupiditate.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/7/relationships/posts","related":"/api/v1/boards/7/posts"}}},"links":{"self":"/api/v1/boards/7"}},{"type":"boards","id":"8","attributes":{"title":"Board Omnis et.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/8/relationships/posts","related":"/api/v1/boards/8/posts"}}},"links":{"self":"/api/v1/boards/8"}},{"type":"boards","id":"9","attributes":{"title":"Board Nesciunt dicta.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/9/relationships/posts","related":"/api/v1/boards/9/posts"}}},"links":{"self":"/api/v1/boards/9"}},{"type":"boards","id":"10","attributes":{"title":"Board Culpa facere ipsum.","created-at":"2017-08-17T13:51:47+0000","updated-at":null},"relationships":{"posts":{"links":{"self":"/api/v1/boards/10/relationships/posts","related":"/api/v1/boards/10/posts"}}},"links":{"self":"/api/v1/boards/10"}}]} 

Do you use Postman? The easiest way to play with the server is Postman.

Run in Postman

API documentation and code snippets here.

As for the Responses I'll answer in the next post.

neomerx commented 7 years ago

Of course, I can provide you with a standalone sample of how to create Responses in Laravel environment. However, you should understand that it is only a fraction of what you really need to make fully functional JSON API server. You will need support for such heavy things as CRUD and validation (tough thing btw), filtering, and pagination. If you are interested I also started with Laravel/Lumen but then had to admit it wouldn't be possible to expect needed changes made in Laravel and Lumen.

neomerx commented 7 years ago

If you have doubts I've got some knowledge in Laravel/Lumen internals have a look at this.

lindyhopchris commented 7 years ago

@pablorsk if it helps, I have a fully running Laravel integration here https://github.com/cloudcreativity/laravel-json-api and a demo app here https://github.com/cloudcreativity/demo-laravel-json-api

neomerx commented 7 years ago

@pablorsk projects from @lindyhopchris is also a good option to consider.

neomerx commented 7 years ago

@lindyhopchris Don't you mind if I take your project for performance comparison as an example of Laravel project?

I had interesting results based on older version and of course, would be excited to compare with a more advanced app such as JSON API server with similar functionality.

pablorsk commented 7 years ago

@neomerx thanks a lot for your help. I know my question is very basic, the only reason is obtain a simple example. We work with laravel + neomerx jsonapi here: http://multinexo.com/ (API documentation here) We have CRUD and a lot of things. But I like work with your library but using PSR-7. Actually we use Laravel without PSR-7.

Having said that, where I found and example how can I set this variables? (no problem of simplicity)

 $outputMediaType = ???; //MediaTypeInterface
 $extensions = new Neomerx\JsonApi\Http\Headers\SupportedExtensions(); // it's rigth?
 $schemes = ???;    // ContainerInterface

About ts-angular-jsonapi, it work fine. But I ask you about an ONLINE backend. If you have, I can put an online demo of my library and use your data and server. Or maybe I don't understand your initial question (sorry I don't speak very well English).

I'm going to take a look at your projects, @lindyhopchris...

neomerx commented 7 years ago

@pablorsk Here is PSR7 sample

{
    "name": "neomerx/responses",
    "require": {
       "zendframework/zend-diactoros": "^1.4",
       "neomerx/json-api": "^1.0"
    }
}
<?php

require_once __DIR__ . '/vendor/autoload.php';

use Neomerx\JsonApi\Contracts\Encoder\EncoderInterface;
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\MediaTypeInterface;
use Neomerx\JsonApi\Contracts\Http\Headers\SupportedExtensionsInterface;
use Neomerx\JsonApi\Contracts\Http\ResponsesInterface;
use Neomerx\JsonApi\Contracts\Schema\ContainerInterface;
use Neomerx\JsonApi\Encoder\EncoderOptions;
use Neomerx\JsonApi\Factories\Factory;
use Neomerx\JsonApi\Http\Headers\MediaType;
use Neomerx\JsonApi\Http\Headers\SupportedExtensions;
use Neomerx\JsonApi\Http\Responses;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\InjectContentTypeTrait;
use Zend\Diactoros\Stream;

class JsonApiResponse extends Response
{
    const HTTP_UNPROCESSABLE_ENTITY = 422;

    use InjectContentTypeTrait;

    public function __construct(string $content = null, int $status = 200, array $headers = [])
    {
        $headers = $this->injectContentType(MediaTypeInterface::JSON_API_MEDIA_TYPE, $headers);

        parent::__construct($this->createBody($content), $status, $headers);
    }

    protected function createBody(string $content = null): StreamInterface
    {
        $body = new Stream('php://temp', 'wb+');

        if ($content !== null) {
            $body->write($content);
            $body->rewind();
        }

        return $body;
    }
}

class AppResponses extends Responses
{
    private $parameters;

    private $encoder;

    private $outputMediaType;

    private $extensions;

    private $schemes;

    private $urlPrefix;

    public function __construct(
        MediaTypeInterface $outputMediaType,
        SupportedExtensionsInterface $extensions,
        EncoderInterface $encoder,
        ContainerInterface $schemes,
        EncodingParametersInterface $parameters = null,
        string $urlPrefix = null
    ) {
        $this->extensions      = $extensions;
        $this->encoder         = $encoder;
        $this->outputMediaType = $outputMediaType;
        $this->schemes         = $schemes;
        $this->urlPrefix       = $urlPrefix;
        $this->parameters      = $parameters;
    }

    protected function createResponse($content, $statusCode, array $headers)
    {
        return new JsonApiResponse($content, $statusCode, $headers);
    }

    protected function getEncoder()
    {
        return $this->encoder;
    }

    protected function getUrlPrefix()
    {
        return $this->urlPrefix;
    }

    protected function getEncodingParameters()
    {
        return $this->parameters;
    }

    protected function getSchemaContainer()
    {
        return $this->schemes;
    }

    protected function getSupportedExtensions()
    {
        return $this->extensions;
    }

    protected function getMediaType()
    {
        return $this->outputMediaType;
    }

    public static function instance(
        array $schemas,
        EncoderOptions $encodeOptions = null,
        EncodingParametersInterface $parameters = null,
        string $urlPrefix = null
    ): self {
        $factory          = new Factory();
        $schemasContainer = $factory->createContainer($schemas);
        $encoder          = $factory->createEncoder($schemasContainer, $encodeOptions);

        $responses = new static(
            new MediaType(MediaTypeInterface::JSON_API_TYPE, MediaTypeInterface::JSON_API_SUB_TYPE),
            new SupportedExtensions(),
            $encoder,
            $schemasContainer,
            $parameters,
            $urlPrefix
        );

        return $responses;
    }
}

$responses = AppResponses::instance([
    // model / schema mapping
]);
lindyhopchris commented 7 years ago

@neomerx I haven't optimized it for performance yet - it's mainly about integrating your package and my additions in a "Laravel" way, and also making sure it's all usable with other Laravel features - e.g. broadcasting, views etc.

The performance of it is totally acceptable for the projects we're using it on at the moment, but it certainly needs optimizing. But will do that when I've settled the package (it's not yet on 1.0 and there's some bits that I'm going to re-write before 1.0).

So no problem you looking at performance, but with the proviso that I know it needs to be optimized (so the results may not be great at this stage).

neomerx commented 7 years ago

@lindyhopchris I'm expecting it would be significantly about how Laravel itself works with routing, authentication, authorization, controllers, models, validation (partly) and other things you can't change.

bravo-kernel commented 7 years ago

@neomerx will you be creating some sort of comparison chart? If so, it might be worth adding this CakePHP implementation: https://github.com/FriendsOfCake/crud-json-api. I can help setting up the system/instructions if need be.

neomerx commented 7 years ago

@bravo-kernel we can actually work together on it :smile: I already have 3 comparisons for limoncello, lumen and slim and ok to accept cake-json-api :wink:

https://github.com/limoncello-php/framework/tree/master/docs/bench

To compare apples to apples let's have some common ground. Both limoncello and cloudcreativity implements similar messaging app (basic boards/sites, posts and comments + users). I'm thinking about 2 scenarios: reading with filters (index) and creation (check validation). If all demos support CORS, Authentication, Authorization I'd like to include them as well.

So it would be like Request -> CORS -> Authorization -> Routing -> Controller -> Filtering/Validation/API -> Response Haven't I miss anyting?

bravo-kernel commented 7 years ago

Looks good to me mr. @neomerx and... basically all out-of-the-box for Cake.

I will see if I can find some time to set up a plain app, let's take it from there.

neomerx commented 7 years ago

@bravo-kernel and it's some basic out-of-the-box limoncello :wink:

bravo-kernel commented 7 years ago

No doubt and I am sure limoncello will be more/better featured ;)

bravo-kernel commented 7 years ago

@neomerx for now I would be most comfortable whooping up a simple example for/using https://github.com/limoncello-php/framework/blob/master/docs/Performance.md.

Additions could be made later, given time or specs. I am a bit fore-warned about creating a full-fledged API (having learned from https://github.com/gothinkster/realworld)

lindyhopchris commented 7 years ago

all sounds good, particularly as it'll give me performance stats to work off when I do get round to optimizing it. I'm prepared to be low down on the stats to start with though ;-)

bravo-kernel commented 7 years ago

A database/model would be a nice thing to start with

bravo-kernel commented 7 years ago

My bad, seems there already is: https://github.com/cloudcreativity/demo-laravel-json-api/tree/5.4/database/migrations

neomerx commented 7 years ago

I've updated the stats for Hello World (limoncello vs lumen vs slim). For the last 3 months limoncello and slim look slightly better (which could be explained by changes in my environment: newer kernel, newer PHP, etc). Lumen looks noticeably worse in absolute RPS numbers which is an unpleasant surprise. I'm a bit worried about coming 5.5 which currently looks like minor changes in syntax, with not enough focus on performance, where the biggest efforts were spent on adding Redis monitor app (Horizon).

pablorsk commented 7 years ago

@neomerx, your example worked perfectly on Laraval + PSR-7. Now, I'll work on a migration from Laravel Routing to PSR-7 Zend\Diactoros\Response of Multinexo.

About ts-angular-jsonapi, if you have an JSON-API online server demo, I need one for a demo propose.

Thanks a lot for your clear and dedicated example for us.

neomerx commented 7 years ago

@pablorsk Thanks for your support. Diactoros do not implement routing so you need something else. Limoncello uses nikic/fast-route for it. I would recommend you start from Core Limoncello component which combines routing (fast route) + HTTP (Diactoros) + container (can use any but pimp is default). It's really compact, elegant and will save you a ton of time. Here is an example.

As for online server demo I'm a bit confused. You may have the demo server on your computer in less than 60 seconds. I posted step-by-step how to above. Let me know if you need any help with it.

neomerx commented 7 years ago

@bravo-kernel @lindyhopchris In case you're curious about limoncello performance for real app (tens of resources, authentication, authorization, CORS). Reading a list of resources with 2 included relationship is tested locally (Nginx, PHP-FPM, MySQL (Percona)) on consumer grade PC. about 1180 RPS with less than 80% CPU usage (I think 1400-1500 can be squeezed if tuned properly)

Running 15s test @ http://localhost/api/v1/ABC?include=DEF,GHI
  12 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    80.85ms   20.56ms 141.55ms   79.75%
    Req/Sec    98.87     27.02   161.00     59.39%
  17802 requests in 15.10s, 83.78MB read
Requests/sec:   1179.11
Transfer/sec:      5.55MB

CPU Usage image

pablorsk commented 7 years ago

Hi again @neomerx, your example was great for my start with PSR-7 + Neomerx/JsonApi on Laravel. I publish an JsonApi server demo (buided for my online Json-Api front-end demo) :tada:

As you see, I only publish GET requests. PUT is not sure if it's ok, because I work directly with $request->getParsedBody() for fill the models. I think your library can check structure, or relationships, etc; and give me a procesed data, rigth? Or I need work directly, for example, with $request->getParsedBody()['data']['attributes']? :confused:

PS: I try to migrate this project with PSR-7 to Lumen, but is not possible adapt it to PSR-7 like Laravel. :disappointed:

neomerx commented 7 years ago

@pablorsk I probably visited it at not the best time and the app didn't respond to my clicks :smile: image

If you start using Limoncello (PSR, CORS, OAuth 2 and other goodies) I can advise and will be very happy to do help.

pablorsk commented 7 years ago

Sorry @neomerx, I forget enable CORS on production :P Now its working (just for get resources and collections). Also, you can use the filter like screenshot :point_right: http://reyesoft.github.io/ts-angular-jsonapi/

Thanks for your recomendation with Limoncello, is very good work. It was checked one year ago with my team. But, on own company we are using Laravel/Lumen in old projects. We can't migrate to Limoncello rigth now :(.

I will try this week use your library for parse PUT/POST data and put into models and save.