thephpleague / fractal

Output complex, flexible, AJAX/RESTful data structures.
fractal.thephpleague.com
MIT License
3.52k stars 352 forks source link

[Question] Role/scope based result in transformers? #327

Open bobmulder opened 7 years ago

bobmulder commented 7 years ago

While using Fractal, I want to base my output on the given scopes or roles from logged in users. My question is, can this be done inside the transformer? Of course, in my workflow it can be done, but I'm not sure if it follows the 'rules' of Fractal.

Example code would be:

$data = [
    'id' => $entity->get('id'),
    'name' => $entity->get('name')
];

if($this->user('role') === 'moderator') {
    $data['status'] = $entity->get('status');
}

Thanks in advance!

Art4 commented 7 years ago

I'm doing the exact same thing.

johannesschobel commented 6 years ago

In my application-scenario I would use different Transformers in my Controller. E.g., there would be a ProductTransformer_Moderator and a ProductTransformer_User. Things could get a bit nasty if you would output different strcutures depending on Roles and/or Permissions, so it may be easier to just have different Transformers.. What do you think?

Furthermore, you would need to inject the currentUser to each Transformer - which kind of binds the Transformer to your User Model..

hegoku commented 5 years ago

Have the same issue... Is there any beautiful solution?

johannesschobel commented 5 years ago

@hegoku , I would suggest to use different Transformers in this use-case. I am not a fan of putting to much logic into the Transformers. Please note that you can extend some kind of BaseProjectTransformer which holds your includes (and the respective relationships) and implement the required transform(...) method in your specific classes..

e.g, like this:

abstract class AbstractProjectTransformer extends Transformer { 
   $availableIncludes = ['your', 'custom', 'includes'];

   includeYour(Project $project) {
      // do some stuff here, like
      return $this->collection($project->something(), new WhateverTransformer());
   }

   // other include methods here
}

class UserProjectTransformer extends AbstractProjectTransformer {
   transform(Project $project) {
      return [
         // your custom structure for USER roles here
      ];
   }
}

class ManagerProjectTransformer extends AbstractProjectTransformer {
   transform(Project $project) {
      return [
         // your custom structure for MANAGER roles here
      ];
   }
}

The proposed solution is way easier to extend (e.g., add new attributes) than constantly adding new if / else statements.. Further, it helps to "decouple" the transformers from the currentUser that called a specific endpoint. Transformers are - as they should be - only classes that transform one class to another one - without having any additional dependencies (like the user that wants to transform the object).

Hope this helps.. all the best

hegoku commented 5 years ago

@johannesschobel This solution is nice, thank you very much!

yoannisj commented 5 years ago

@johannesschobel , I used this approach and mixed in some advanced transformation with includes, which adds some nice extra flexibility to my APIs endpoints.

BUT.. I am have trouble making this work with collection, which contains multiple types of items (models) and therefor should delegate the transformation of each of the items to a different transformer.

To continue with the user example, when including a collection of users with different roles, how do I tell fractal to use a different transformer based on the user's role, and make sure nested includes are passed through?

I have been stuck on this problem for days, and can't find a solution. Thanks for anybody's help!

johannesschobel commented 5 years ago

@yoannisj , what exactly do you mean by

collections, which contains multiple types of items

?

Do you mean something like this:

$data = [
   $book1,
   $author1,
   $book2,
   $car,
   $user1,
   $user2,
   // ...
];

Are you actually mixing different resources in one response? if yes, i don't think that this is the desired behaviour or how you should implement it.. Usually you have one "root" resource (e.g., determined by the URL, /api/books) and the other resources should be "embedded" (e.g., through the includesX methods..

Hope this helps

yoannisj commented 5 years ago

@johannesschobel, I am building an API for a website which contains listing/index pages, including a list of entries (a.k.a. posts). The endpoint to that page /api/pages/some-listing-page?include=entries returns something like so:

$data = [
    "title" => "Some listing page",
    "url" =>"https://www.example.com/some-listing-page",
    "postDate" => "2019-05-08T12h30"
    "intro" => "Lorem ipsum dolor…",
    "entries" => [
        $newsItem, $article, $article, $event, $newsItem, $article, /* ... */
    ],
    /* ... */
];

The "root" transformer for that page loads the data for the "entries" key, through an includeEntries method, which returns a fractal CollectionResource. But, the data-structure of the entry items in that collection can differ quite a lot from one entry type to another, some having their own nested includes (for example an article entry could respond to "?include=entries.writers", and an event entry could respond to another"?include=entries.location").

The examples I have seen in the documentation, seem to suggest that I can only pass a single transformer class to the collection returned in the page transformer's includeEntries method. Something like:

public function includeEntries( $page )
{
    $entries = new Query('entries')->parentPage($page)->fetch();

    return $this->collection($entries, new EntryTransformer());
}

But that would mean, that the EntryTransformer needs to implement the data transformation logic for all the different entry types, and all the possible nested includes in one single transformer class, which gets messy very quickly.

Ideally, there would be a way to tell Fractal to:

Is that possible, and how can I achieve such behaviour? Maybe the answer is to rely on a common PolyEntryTransformer class, which gets passed to the included collection of entries, and then instanciates a sub-transformer for each entry, while passing in the nested include scope? How would that look like?

Sorry, this starts to look more and more like a stack overlfow ticket, rathat than a github issue. I would be happy to re-iterate my question in a more appropriate channel.

Thanks a lot for the help!

yoannisj commented 5 years ago

Seems to be a duplicate of #244

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 4 weeks if no further activity occurs. Thank you for your contributions.