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

[RFC] ZfrNext #183

Open
bakura10 opened this issue Jan 6, 2015 · 10 comments
Open

[RFC] ZfrNext #183

bakura10 opened this issue Jan 6, 2015 · 10 comments

Comments

@bakura10
Copy link
Member

bakura10 commented Jan 6, 2015

This post replaces the previous one and tries to formalise a bit more what I have in mind for next ZfrRest (that I will start developing once we have ZF3 components, especially refactored router). Those changes are based on my experience in using ZfrRest for a moderately complex API in production.

Error handling

ZfrRest uses its own set of exceptions based on HTTP error. A listener is used to automatically catch the error, and generate a coherent error message that includes status_code, error message and additional information (in case of validation).

It also has the ability to configure an exception_map that automatically maps a user-land exception to a ZfrRest exception.

In overall, I especially like this part of ZfrRest. It works as expected, and I have no intention to modify it.

Routing/controller

Current ZfrRest provides an automatic routing through the ResourceGraphRoute. This route reads the Doctrine mapping and ZfrRest mapping, and uses the Criteria API to recursively filter the resources.

Advantages

Simplicity

For simple use cases (few resources with very simple associations), this works well and allows to quickly build an API with a few ZfrRest annotations.

RAD

Because resources are fetched transparently in the route, you receive a ready-to-consume entity in your controler. In case of POST or PUT, incoming data has already been validated, filtered, and hydrated. This means that your actions are very lightweight (basically, just a permission call and a service update).

Drawbacks

In real-life applications though, current ZfrRest architecture proved to be highly frustrating.

Force to introduce bi-directional associations

Because accessing resources is made through associations traversal, ZfrRest forces you to introduce associations that may not make sense or may complicate your domain model. For instance, I had in my application a "Project" entity. Each projet could have multiple "members". However, I originally did not want to include the bi-directional associations (and instead keep it one-sided, with the "Member" entity having an association to a Project).

However, for the "/projects/:id/members" to be accessible, I needed to introduce this non-necessary association.

Unecessary database calls

Assuming the following path "/users/4/tweets", the router would have first matched the users 4 (one database call), and then returned a PersistentCollection to tweets. The persistent collection does not generate API calls until it is traversed (allowing you to filter it without loading all the tweets). However, this still does 2 database calls, while it could have been done in 1.

The problem is more visible for URI that are even more nested: /users/4/tweets/5/retweets. While not necesarily the best practice, URIs could be nested like that, and this one would generate 3 database calls, while an optimized service could do that in 1.

The reason is that the route is a recursive process, and cannot know how to perform optimized DB calls as it uses the simple Criteria API.

Can lead to highly inefficient code

In my application, I have a "Project" entity, and each team member has their own set of settings for the project. I want my API to return something like that:

{
   "projects": [
     {
       "id": 1,
       "settings": {
         "id": 4,
         "user": 3,
         "language": "fr"
       }
     },
     {
       "id": 2,
       "settings": {
         "id": 5,
         "user": 3,
         "language": "fr"
       }
     }
   ]
}

Depending on the user, the "settings" is filled with the specific data of the user. Indeed, it is not possible to introduce a "settings" association to the Project entity because it depends on the user.

In ZfrRest, this resource can be acceded through the "/users/4/projects" endpoint. The controller receives a Collection of projects, and you then need to iterate through them, and fetching the settings based on the logged user. If you have 10 projects, this would mean 10 DB calls, while this could be done in 1 JOIN.

Once again, the problem comes from the fact that ZfrRest automatically gives you resources, and prevents you to use an optimized service layer.

Proposal

I suggest completely removing the routing layer of ZfrRest, and instead replace it by an explicit routing. This document uses a Dash-like syntax.

I'd like if ZF3/next completely removes the AbstractRestfulController, and just comes with an AbstractActionController.

In overall, the ZfrRest controller/routing layer will be a better ZF2 AbstractRestfulController.

Routing

Current Dash config assumes "path" / "action" / "controller", but I think this should be changed for "path" / "controller" / "action". This proposal assumes this order.

Simple routes

For simple routes, it works like any action controller, except that the action is ommited:

return [
    'routes' => [
        'users' => ['/users', UserListController::class]    ]
];

This route would be used to dispatch a collection URI (like POST /users, GET /users). The controller would automatically dispatch to the method based on the HTTP verb. If no matching method could be used, the controller would trigger a MethodNotAllowedException to return a 405 error.

For OPTIONS method, a built-in method will be present in the base controller.

Details route

Now, if we want to access a single user:

return [
    'routes' => [
        'users' => ['/users', UserListController::class, 'children' => [
            'user' => ['/:user_id', UserController::class]
        ]]
    ]
];

If hitting the /users/3 URI, the request will be dispatched to the UserController instead, and would follow the same rules.

Nested routes

Nested routes would work as you would expect them to work:

return [
    'routes' => [
        'users' => ['/users', UserListController::class, 'children' => [
            'user' => ['/:user_id', UserController::class, 'children' => [
                'tweets' => ['/tweets', UserTweetListController::class, 'children' => [
                    'tweet' => ['/:tweet_id', UserTweetController::class]
                ]]
            ]]
        ]],

        'tweets' => ['/tweets', TweetListController::class]
    ]
];

Contrary to ZF2 AbstractRestfulController, ZfrRest AbstractRestfulController will allows you to pass different named parameters (instead of forcing you to name all identifiers "id").

Dash compact syntax (assuming that some changes are made on the default order - ping @DASPRiD) allows to make this very usable and explicit. Contrary to ZfrRest, you can very easily map a different controller depending on wether you access a resource directly, or through a nested URI (here, you can see that tweets is mapped either to UserTweetListController or TweetListController).

Injection

For simplicity, ZfrRest will not try to automatically inject repository or service based on a resource name. As all controllers are fetched from service manager, you are responsible to inject the right service.

Controller verbs

In ZfrRest, you used to have code like this:

public function get(User $user)
{
   // Do what you want
}

In ZfrRest NEXT, it will mostly behave like a normal action controller:

public function get()
{
    $userId = $this->params()->fromRoute('user_id');

    // Fetch from your service
}

RPC like action

Sometimes, it is actually useful to be able to define actions, even in a REST context, as not everything map to REST properly. For instance, a "start import" cannot be expressed as a HTTP verb that easily.

In ZfrRest this was frustrating, because you were forced to create a different controller, with a different factory. This leaded to a lot of duplication code.

I suggest that ZfrRest controller actually supports both things by extending the default AbstractActionController. For instance:

return [
    'routes' => [
        'projects' => ['/projects', ProjectListController::class, 'children' => [
            'project' => ['/:project_id', ProjectController::class, 'children' => [
                'start-import' => ['/start_import', null, 'startImport']
            ]]
        ]]
    ]
];

Data validation/filtering

In ZfrRest, the validation and filtering was done automagically:

  1. You would set an input filter and hydrator in the mapping.
  2. If POST/PUT, ZfrRest would automatically extract the data using the bound hydrator, validate it and hydrate it.
  3. Resulting entity would be pass to the controller.

Once again, this works quite well for simple cases, but falls up completely for more complexe use cases. I'd suggest that we'd go back to explicitness again, with some shortcuts for simplicity.

Validation/filtering

Before refactoring ZfrRest, I'll probably refactor input filters. What I'd like to add is a better validation groups, and more importantly "named" validation group. Each named validation group will actually contain a list of fields to validate, plus additional context for dynamic based validation/filtering. ZfrRest controller would offer a shortcut to avoid manually fetching the input filter manager, creating it, setting it...

public function post()
{
    $values = $this->validateIncomingData(TweetInputFilter::class, 'create', ['user' => $this->identity()]);
}

Internally, the validateIncomingData will be responsible to:

  1. Fetching the data from POST, and converting it into a PHP array (it will only support JSON for now but may support XML as well).
  2. Retrieving the input filter given from the input filter plugin manager.
  3. Set the named validation group (here, 'create').
  4. Validate the data using the given context (here, we pass the given user, that could be used to dynamically exclude/include some data based on the user's role, for instance).
  5. Throwing a UnprocessableEntity (422 error) if the input filter fails.
  6. Returning the filtered values if success.

Data hydration

Once again, new ZfrRest will go explicit. Similarily, controllers would provide a shortcut for hydrating data:

public function post()
{
    // ...

    $object = new MyObject();
    $object = $this->hydrateForClass(TweetHydrator::class, $object, $values, ['user' => $this->identity()]);
}

Once again, this is just a shortcut for:

  1. Getting the hydrator plugin manager.
  2. Fetching it.
  3. Calling the hydrate method, and handle any possible error that would occur.

Both validateIncomingData and hydrateForClass would no nothing magic, those are just shortcut to reduce boilerplate code.

Typical GET request

public function get()
{
   $tweet = $this->tweetService->getById($this->params()->fromRoute('tweet_id'));

   // ... See section about view
}

Typical DELETE request

public function delete()
{
   $tweet = $this->tweetService->getById($this->params()->fromRoute('tweet_id'));
   $this->tweetService->delete($tweet);

   // ... See section about view
}

Typical POST request

public function post()
{
   $values = $this->validateIncomingData(TweetInputFilter::class, $values /*, $context */);

   $tweet = new Tweet();
   $tweet = $this->hydrateForClass(TweetHydrator::class, $tweet, $values /*, $context */);

   $tweet = $this->tweetService->create($tweet);

   // ... See section about view
}

Typical PUT request

public function put()
{
   $values = $this->validateIncomingData(TweetInputFilter::class, $values /*, $context */);

   $tweet = $this->tweetService->getById($this->params()->fromRoute('tweet_id'));
   $tweet = $this->hydrateForClass(TweetHydrator::class, $tweet, $values /*, $context */);

   $tweet = $this->tweetService->update($tweet);

   // ... See section about view
}

All of this is more verbose than current ZfrRest, but at least we keep control about what happen (I suspect that as we remove all the magic, performance will be drastically better too). This also solves cleanly different validation rules based on method.

Resource rendering

In current ZfrRest, the rendering is automatically done through the the bound hydrator, and a specific rendering strategy.

Advantages

  • Once again, for simple use cases, it does everything automatically, and this works pretty well... but advantages barely stop here.

Drawbacks

  • It is complex to provide custom rendering: because everything is automatic, you are force to create your own rendering strategy, and copy-pasting a large amount of code, just for customizing a simple thing.
  • Can lead to horrible performance if not taken care of: because the current renderer is automatic, what it does is basically iterating through all the fields, outputing them, then iterating through all the relations and outputing them. However, for large associations, this can perform SQL queries you may not expect, just because it wants to render all the fields. You are therefore force to go into the entity, add a specific mapping on the association just to say ZfrRest NOT to output this association. Annoying.
  • It may output unwanted fields: similarily, remember that ZfrRest forces you to introduce bi-directional associations if you want to take advantage of routing... but as a consequence, all those assocations introduced purely for routing, will also be outputed in JSON result.
  • Slow as hell: check the code of the renderer, you will understand. It's a complete mess of Doctrine's ClassMetadata calls.
  • Hard to implement versioning: because everything is handled automatically, versioning is nearly impossible.

Proposal

I actually suggest a new concept called a ResourceModel. This is actually a hybrid thing between JsonModel and ViewModel, and will look like a .phtml template, instead that it will output your model. At its simplest, you will return a ResourceModel like you would return a ViewModel:

public function get()
{
    $tweet = $this->tweetService->getById($this->params()->fromQuery('tweet_id'));

    return new ResourceModel(['tweet' => $tweet, 'context1' => 'value1', ...]);
}

Similarily as a view model, it will actually look for a template (I suppose in the "views" folder, but we may decide something else). Its name will be infered from the route hierarchy and method.

Assuming the following route config:

return [
    'routes' => [
        'tweets' => ['/tweets', TweetListController::class, 'children' => [
            'tweet' => ['/:tweet_id', TweetController::class]
        ]]
    ]
];

It will look for a template called "get.php":

-- views
    -- tweets
        -- tweet
            -- get.php

This file will be very simple, and you will need to return a simple PHP array (yeah, 100% control!). Here is an example of such a file (assuming a "tweet" context and "user" that is the logged user):

$data = [
    'id' => $tweet->getId(),
    'content' => $tweet->getContent(),
    'writer' => $tweet->getOwner()->getId()
];

foreach ($tweet->getLatestRetweets() as $retweet) {
    $data['retweets'][] = [
        'id' => $retweet->getId(),
        'retweeted_by' => $retweet->getUser()->getId()
    ];
}

if ($user->isAdmin()) {
    $data['internal_id'] = $tweet->getInternalId();
}

return $data;

As you can see, templates do not contain any complex logic. It's basically controlling how the data is rendered. Because of the context, it allows you to include or refused some fields depending on the context.

Post-processors

Often, responses include a lot of common patterns. For instance, some API always wrap the response under a "data" key. When paginating, you always want to include a "total_count", "offset" and "limit" properties... and so-on.

Obviously, manually writing those would be cumbersome. You can therefore register post-processors (and create your own), inside the ZfrRest config:

return [
    'zfr_rest' => [
        'post_processors' => [
            ResponseWrapperPostProcessor::class,
            PaginationPostProcessor::class,
            UnderscorePostProcessor::class
        ]
    ]
];

Because order is important, you will be able to set "priority". Internally, I suspect that those post_processors will be implemented as listeners inside the Rendering Strategy.

At its simplest, the PostProcessor interface looks like this:

interface PostProcessor
{
    public function postProcess(array $data);

    public function isEnabled();
}

Each post-processor can be enabled/disabled directly in the view, and configured.

For instance, if we want to always wrap data around a "data" key, we could write our post-processor:

class DataWrapperPostProcessor implements PostProcessor
{
    public function postProcess(array $data)
    {
        return ['data' => $data];
    }

    public function isEnabled()
    {
        return true; // always enabled
    }
}

For instance, let's assume that we are using a "GET" and that we want to wrap the payload around a common "data" key:

$data = [
    'id' => $tweet->getId()
    // ...
];

return $data;

Because this is always registered, it will automatically wrap the result.

Let's take another example with a Paginator post-processor, that we need to enable whenever we have a collection resource:

// Prepare your data

$this->enablePostProcessor('name')->setPaginator($paginator);

return $data;

Boom, the post-processor will execute.

Versioning

ZfrRest will be able to support simple versioning by dynamically changing the template. There will be two ways to configure it: either using a special parameter in the ResourceModel, or using headers. For instance, using headers, you will configure a specific Accept-Content to a version:

return [
    'zfr_rest' => [
        'versions' => [
            'vnd2' => 'v2'
        ]
    ]
];

Now, instead of fetching the default view, it will check according to a based folder called "v2" (or whatever you prefer). If I take my previous example, it will look for this file:

-- views
    -- v2
        -- tweets
            -- tweet
                -- get.php
    -- tweets
        -- tweet
            -- get.php

If ZfrRest does not find the "v2", it will fallback to the default template. Alternatively, you can force a specific version in the controller by using a second parameter:

return new ResourceModel($context, 'v2');

For instance, let's take we have a "cards" resource that will output the credit card resource. The default template will be:

return [
    'id' => $card->getId(),
    'last4' => $card->getLast4(),
    'country' => $card->getLast4(),
    'type' => $card->getType()
];

Now, let's say that we release a v2, that output the "type" as "brand" instead. We therefore create our new template inside the v2 folder:

return [
    'id' => $card->getId(),
    'last4' => $card->getLast4(),
    'country' => $card->getLast4(),
    'brand' => $card->getType()
];

While this work, there is a lot of duplicate. Therefore, the renderer will support two helpers methods: renderDefaultVersion' andrenderVersion`. Previous example could be re-written as:

$data = $this->renderDefaultVersion();
$data['brand'] = $data['type'];
unset($data['type']);

return $data;

If we release a v3 that depends on v2:

$data = $this->renderVersion('v2');
// Do changes

return $data;

Conversion to JSON

At the very end of the process (once all the post-processor have been run), ZfrRest will convert the output to JSON, and return the response to the client.

End rendering

ZfrRest takes care of properly setting the response status code for GET, POST, PUT and DELETE methods. It is currently doing it in the MethodHandler.

However, I intend to do this in a listener, that will likely hook to the finish event. Based on the method, and the result, it will properly set the headers.

Thoughts?

ping @Ocramius @danizord @mac_nibblet @Thinkscape @blanchonvincent

@Ocramius
Copy link
Member

Ocramius commented Jan 6, 2015

You'll have to ping me later on for this, as I currently don't have time to get to it :-(

@bakura10
Copy link
Member Author

bakura10 commented Jan 6, 2015

I will, I definitely need feedback on this :).

@bakura10
Copy link
Member Author

I will likely implement this much sooner than expected considering how ZfrRest restricts me currently, so if someone has some time reading this, especially the part related to rendering, it would be very helpful.

Ping @danizord @Ocramius

@bakura10
Copy link
Member Author

Hi everyone,

I have given more thoughts to how ZfrRest should evolve. I have open a refactor branch, where I have kept what I'm happy with (errors handling), and throw out everything else (basically).

The controller part is now much more simpler and dumb than before. There is no longer automatic routing, transparent validation... but some helpers are here to make your life easier. The only thing I've made is automatically injecting into methods the matched route params (as those are nearly always needed for fetching resources using services). Here is a simple example using the new controller:

class UserController extends AbstractRestfulController
{
    private $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function get($params)
    {
        $user = $this->userService->getById($params['user_id']);

        return new ResourceViewModel(['user' => $user]);
    }

    public function delete($params)
    {
        $user = $this->userServcie->getById($params['user_id']);
        $this->userService->delete($user);
    }
}
class UserListController extends AbstractRestfulController
{
    private $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function post()
    {
        $data = $this->validateIncomingData(UserInputFilter::class);
        $user = $this->hydrateData(UserHydrator::class, new User());

        $this->userService->create($user);

        return new ResourceViewModel(['user' => $user]);
    }

    public function put($params)
    {
        $data = $this->validateIncomingData(UserInputFilter::class);

        $user = $this->userService->getById($params['user_id']);
        $user = $this->hydrateData(UserHydrator::class, $user);

        $this->userService->create($user);

        return new ResourceViewModel(['user' => $user]);
    }
}

Plain and simple. Contrary to ZF2 Abstract Restful controller, you are not restricted to one-level of URI, as you receive all the route params. You can now performs optimized queries, using your own service layer, fetching related entities...

Rendering

Rendering was the part I was not sure about. So I listen to what some people needed, what I actually always need when building an API, and came to a list of features I absolutely want:

  • Not being restricted with a resource of one type (aka the problem that required to introduce associations just for rendering purposes in previous ZfrRest). For instance, if I have a "User" resource, I must be able to add a "billing" sub-resource, even if "billing" is not part of the user entity (typical use case of One-to-one associations).
  • A lot of APIs I'm using, and what I need myself too, is being able to include sub-resources. Example: I want to be able to retrieve users alone ("/users/1"), but also with their associated projects ("/users/1?include[]=projects"). This is critical for good performance, and something ZfrRest struggles currently because of all its automatism.
  • Being able to post-process a response. Typical use case include the needs to restrict which fields to return (to save bandwith). In API, this is usually done using a "fields" attribute: "/users/1?fields[]=first_name&fields[]=last_name"

ResourceViewModel

The resource view model is a sub-class of ViewModel and works very similar. The first parameter is a list of variables. This is what allows to render different resources into the same payload.

The second parameter is a list of options, and currently accepts the following ones:

  • include: an array of strings
  • fields: an array of strings
  • wrap_key: an optional string that can be used to wrap the payload
  • version: an optional version to use (default to "default")
  • strict_version: default to false

Some other may be introduced in the future, and while it may make some people unhappy, I want to favour "pragmatic" usage over a strict usage.

ResourceRenderer

The resource renderer will map a ResourceViewModel to a PHP template file. Template files will be map according to their route name. For instance, considering the following routes:

'users' => ['/users', UserListController::class, 'children' => [
    'user' => ['/:user_id', UserController::class, 'children' => [
        'tweets' => ['/tweets', UserTweetListController::class, 'children' => [
        'tweet' => ['/:tweet_id', UserTweetController::class]
    ]]
]],

'tweets' => ['/tweets', TweetListController::class]

If you want to render a tweet that belongs to a user, the renderer will look for the template called "default.users.user.tweet". The first part is actually the version (default to "default").

You may mix multiple versions. For instance, you may want to render the "user" differently (but only user). In this case, you could introduce a new template called "v2.users.user". Based on the extracted version, ZfrRest will select this one.

If it does not find a specific template for a given version, it will either choose the default one if strict_version is false (default), or throw an exception if strict_version is true.

Resource Strategy

The resource strategy will be enabled whenever it encounters a "ResourceViewModel" instance. Otherwise, it is not executed and still allows you to render other types like JsonModel.

Template

Now, the interesting part: the template. I actually tried several syntaxes, until I find one that is flexible enough and fast to write. This has been made possible with the usage of new view helpers (or renderer methods, I don't know yet how implementation will work):

  • renderResource($hydratorName, $resource): allow to render a given object using a resource
  • renderCollection($hydratorName, $resources): render a collection of resources using a hydrator
  • renderTemplate($templateName, array $context): allow to render a named resource template using another context. This allows easy composition (for instance, in your v2 template, you may decide to render the v1 template, and change only one single field).
  • includeTemplate($includePath, $templateName, array $context): similar to renderResourceTemplate, but with an additional include path (more on that later).

Let's take an example: I want to render a user, with their associated tweets, and for each tweets, their retweets, and also render the user billing info (that is not part of the user class but was loaded using a JOIN within an optimized repository, for instance). Here is how it looks like:

// Render the user itself
$this->renderResource(UserHydrator::class, $this->user);

// If there is a specific endpoint for getting billing info:
$this->renderTemplate('users.user.billing', $this->billing);

// Or we can use using a hydrator too:
$this->renderResource(UserBillingHydrator::class, $this->billing);

// Render a template using an include
if ($this->hasInclude('tweets')) {
    $this->includeTemplate($this->getInclude('tweets'), 'tweets', ['tweets' => $user->getTweets()]);
}

As you can see, this approach and the use of recursive template has a nice side-effect: you no longer need to write complex hydrators that implement recursive extraction, because this is handled through the template (which is much more logical IMO).

The include template is like renderTemplate, but instead, it understand a "dot" notation. For instance, you could pass the include "tweets.retweets". When calling "hasInclude('tweets')" it will return true because the "tweets.retweets" include "tweets".

This will render the "tweets" template, using the new context, but the include will be "changed" to "retweets" only, which allow you to have this in your tweets template:

// We could also use $this->renderCollection, but as we want to handle include, we need
// to render a separate template for more control
foreach ($this->tweets as $tweet) {
    $this->renderTemplate('tweets.tweet', ['tweet' => $tweet])
}

And finally the tweets.tweet template:

$this->renderResource(TweetHydrator::class, $this->tweet);

if ($this->hasInclude('retweets')) {
    $this->renderTemplate('retweets.retweet', ['retweets' => $this->tweet->getRetweets())];

    // If retweets had also a possibility of include, we could use an includeTemplate instead
    $this->includeTemplate($this->getInclude('retweets'), 'retweets', ['retweets' => $this->tweet->getRetweets()])
}

Let me know what you think, if you have a better syntax idea....

ping @danizord @Orkin

@bakura10
Copy link
Member Author

Hi everyone,

I've had a talk with @Orkin, and he found the view layer confusing. Instead, he suggests skipping the hydrator for rendering (which kinda makes sense as the view is already a "rendering purpose"). This means that in ZfrRest context, it makes "extract" method useless.

He therefore suggests something completely explicit. Here is an example of a view:

$data  = [];

foreach ($this->users as $user) {
    $data[] = $this->renderTemplate('users.user', ['user' => $user]);
}

return $data;

Or a single user:

$data = [
   'first_name' => $this->user->getFirstName(),
   'last_name' => $this->user->getLastName()
];

// A related resource
if (isset($this->billing)) {
   $data['billing'] = [
       'card' => $this->billing->getCard()
   ]
}

// Tweets of the user

foreach ($this->user->getTweets() as $tweet) {
    $data['tweets'][] = $this->renderTemplate('tweets.tweet', ['tweet' => $tweet]);
}

To me it is essential to keep a "renderTemplate" that allows to render another named template, to avoid code duplication. What do you think?

@Orkin
Copy link
Member

Orkin commented Jan 19, 2015

I think it's better doing like this ;)

Le 19 janv. 2015 à 19:06, Michaël Gallego notifications@github.com a écrit :

Hi everyone,

I've had a talk with @Orkin, and he found the view layer confusing. Instead, he suggests skipping the hydrator for rendering (which kinda makes sense as the view is already a "rendering purpose"). This means that in ZfrRest context, it makes "extract" method useless.

He therefore suggests something completely explicit. Here is an example of a view:

$data = [];

foreach ($this->users as $user) {
$data[] = $this->renderTemplate('users.user', ['user' => $user]);
}

return $data;
Or a single user:

$data = [
'first_name' => $this->user->getFirstName(),
'last_name' => $this->user->getLastName()
];

// A related resource
if (isset($this->billing)) {
$data['billing'] = [
'card' => $this->billing->getCard()
]
}

// Tweets of the user

foreach ($this->user->getTweets() as $tweet) {
$data['tweets'][] = $this->renderTemplate('tweets.tweet', ['tweet' => $tweet]);
}

To me it is essential to keep a "renderTemplate" that allows to render another named template, to avoid code duplication. What do you think?

Reply to this email directly or view it on GitHub.

@danizord
Copy link
Contributor

@bakura10 Sorry, I'm very busy these days 😪. But I'll have a deep look this weekend ;)

@bakura10
Copy link
Member Author

Thanks!

@Mischosch
Copy link

I really like your origin thoughts about ZfrRest as it reflects many problems I got while working with ZF2 on apis. Did not have the chance to tryout ZfrRests - but this is on my todo list now!

I'm def a fan of less magic - having simple route definition sounds much better then having magic inside that part. I would prefer using the same style, that zf3 is going to use (would wonder, if its not the solution DasPRID was working on? Is dash style there used, too?) - but 👍 for that!

ResourceViewModel sounds like a great way to change the format of output. I had that problem several times with client projects and ended up with some dirty hacks, to get the schema, that frontend/managers/documentation wants.

Post-processors for field filter: perfect.

This all reads like a whishlist for Doctrine/ZF2 based API development. Tried apigility, but Doctrine integration somehoew feels a little bit strange there. Really looking forward to test out ZfrRest.

Good and very transparent work in writing down your thoughts.

@bakura10
Copy link
Member Author

Thanks @Mischosch . Unfortunately, automatism can't work when you need efficient queries, flexible rendering... Anyway, the code is already ready in this branch #184

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

No branches or pull requests

5 participants