neomerx / json-api

Framework agnostic JSON API (jsonapi.org) implementation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Example of \Neomerx\JsonApi\Http\Responses use

pablorsk opened this issue Β· comments

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?

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).

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

@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.

@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:

  • MediaTypeInterface $outputMediaType
  • ContainerInterface $schemes: I have $encoderArray, but dont find how convert to ContainerInterface

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);
});

@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.

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.

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

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

@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.

@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...

@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
]);

@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).

@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.

@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.

@bravo-kernel we can actually work together on it πŸ˜„ I already have 3 comparisons for limoncello, lumen and slim and ok to accept cake-json-api πŸ˜‰

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?

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.

@bravo-kernel and it's some basic out-of-the-box limoncello πŸ˜‰

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

@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)

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

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

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).

@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.

@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.

@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

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) πŸŽ‰

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']? πŸ˜•

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

@pablorsk I probably visited it at not the best time and the app didn't respond to my clicks πŸ˜„
image

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

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 πŸ‘‰ 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.