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

Avoid data in included elements? #90

Closed
lowerends opened this issue Aug 6, 2014 · 24 comments
Closed

Avoid data in included elements? #90

lowerends opened this issue Aug 6, 2014 · 24 comments

Comments

@lowerends
Copy link

Is there a way to not have the data key as part of the included elements, while still keeping it for the top level response data?

@sangar82
Copy link

sangar82 commented Aug 8, 2014

You can use Serializers to achieve this
http://fractal.thephpleague.com/serializers/

@lowerends
Copy link
Author

Thanks, that will do.

@lowerends
Copy link
Author

I've written a custom serializer to try to solve my problem, and if I understand correctly, I would need to implement this behaviour in the serializeIncludedData function. My custom serializer looks like this:

<?php 

namespace App\Extensions\Serializers;

use League\Fractal\Pagination\CursorInterface;
use League\Fractal\Pagination\PaginatorInterface;
use League\Fractal\Serializer\SerializerAbstract;

class CustomSerializer extends SerializerAbstract
{
    /**
     * Serialize the top level data.
     * 
     * @param  array  $data
     * @return array
     */
    public function serializeData($resourceKey, array $data)
    {
        return ['data' => $data];
    }

    /**
     * Serialize the included data
     * 
     * @param  string  $resourceKey
     * @param  array  $data
     * @return array
     **/
    public function serializeIncludedData($resourceKey, array $data)
    {
        return $data;
    }

    /**
     * Serialize the meta
     * 
     * @param  array  $meta
     * @return array
     */
    public function serializeMeta(array $meta)
    {
        if (empty($meta)) {
            return array();
        }

        return array('meta' => $meta);
    }

    /**
     * Serialize the paginator
     *
     * @param \League\Fractal\Pagination\PaginatorInterface $paginator
     * @return array
     **/
    public function serializePaginator(PaginatorInterface $paginator)
    {
        $currentPage = (int) $paginator->getCurrentPage();
        $lastPage = (int) $paginator->getLastPage();

        $pagination = array(
            'total' => (int) $paginator->getTotal(),
            'count' => (int) $paginator->getCount(),
            'per_page' => (int) $paginator->getPerPage(),
            'current_page' => $currentPage,
            'total_pages' => $lastPage,
        );

        $pagination['links'] = array();

        if ($currentPage > 1) {
            $pagination['links']['previous'] = $paginator->getUrl($currentPage - 1);
        }

        if ($currentPage < $lastPage) {
            $pagination['links']['next'] = $paginator->getUrl($currentPage + 1);
        }

        return array('pagination' => $pagination);
    }

    /**
     * Serialize the cursor
     * 
     * @param  \League\Fractal\Pagination\CursorInterface  $cursor
     * @return array
     **/
    public function serializeCursor(CursorInterface $cursor)
    {
        $cursor = array(
            'current' => $cursor->getCurrent(),
            'prev' => $cursor->getPrev(),
            'next' => $cursor->getNext(),
            'count' => (int) $cursor->getCount(),
        );

        return array('cursor' => $cursor);
    }
}

Changes to the serializeData function are reflected in the output but changes to the serializeIncludedData function don't seem to have any effect. That function is not even called. Am I doing something wrong? I'm using version 0.8.3 in combination with https://github.com/dingo/api.

@lowerends lowerends reopened this Aug 8, 2014
@lowerends
Copy link
Author

Any ideas, anyone?
Is the serializeIncludedData supposed to work?

@LeeMcNeil
Copy link

+1 on this, attempted to create a custom serializer but the includeData method is never called.

@philsturgeon
Copy link
Member

Be careful of which version you are using. I removed the serialize prefix from the serialize methods because it was superfluous.

Sounds like you've both got different versions as one is using serializeIncludedData and another is using includeData.

@AlexDubstone
Copy link

Same problem here.

I'm using fractal 0.8.3 with Dingo/Api and it's not possible to create a custom serializer the way I want it.
The toArray method in the Scope class is calling serializeDatawhen it should call serializeIncludedData.

Therefore it is not possible to remove the "data"-key in included elements.

@mediaholding
Copy link

Same in version 0.9.*.

The includedData() method is only called in the case if sideloadIncludes() returns true.

In other case it is not possible to change default behavior and remove annoying "data" key for embedded data.

@philsturgeon
Copy link
Member

So somebody should fix this. Between you there has to be somebody who can get a PR over to me. I have zero free time for this for the next few weeks.

I'd really appreciate it.

@mediaholding
Copy link

The solution won't need to change Fractal code. If you wouldn't like to have your response wrapped with 'data' key create a new serializer or extend existing one and override following methods like that:

    public function collection($resourceKey, array $data)
    {
        return ($resourceKey && $resourceKey !== 'data') ? array($resourceKey => $data) : $data;
    }

    public function item($resourceKey, array $data)
    {
        return ($resourceKey && $resourceKey !== 'data') ? array($resourceKey => $data) : $data;
    }

It's easy and works perfectly!

@AlexDubstone
Copy link

Please note that this works only with fractal version 0.9.x. As dingo/api does not support 0.9.x yet I had to write a rather hacky workaround. This code works for me but !!is completely untested!!

Like @mediaholding said, write your own serializer and override the serializeData method.

    /**
     * Serialize the data.
     *
     * @param  array  $data
     * @return array
     */
    public function serializeData($resourceKey, array $data)
    {
        // todo update fractal to 0.9.x and remove this workaround (and probably the whole method)
        // We have to use a workaround since fractal 0.8.* apparently has no other
        // way to remove the data attribute from included elements
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5);

        if($backtrace[2]['function'] === 'processIncludedResources')
        {
            return $data;
        }

        return array('data' => $data);
    }

@boukeversteegh
Copy link

Based on @mediaholding I am using the following solution. Notice that by default, when $resourceKey === null the serializer will create data, as per original implementation.

/**
 * Create a new Serializer in your project
 */
use League\Fractal\Serializer\ArraySerializer;

class DataArraySerializer extends ArraySerializer
{
    public function collection($resourceKey, array $data)
    {
        if ($resourceKey === false) {
            return $data;
        }
        return array($resourceKey ?: 'data' => $data);
    }

    public function item($resourceKey, array $data)
    {
        if ($resourceKey === false) {
            return $data;
        }
        return array($resourceKey ?: 'data' => $data);
    }
}
# Set the serializer on fractal manager
$manager = new League\Fractal\Manager();
$manager->setSerializer(new DataArraySerializer());

# Or if you are using the ellipsesynergie/api-response package for Laravel
$this->response->getManager()->setSerializer(new DataArraySerializer());

It is fully backwards compatible, except that if you pass false as a resource key, it does not generate a {"data": .... } wrapper.

For example, I have this transformer:

class FriendshipTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'user',
    ];

    public function transform(Friendship $friendship)
    {
        return $friendship->toArray();
    }

    public function includeUser(Friendship $friendship)
    {
        // pass false as resourceKey
        return $this->item($friendship->user, new UserTransformer, false);
    }
}

Edit: fixed issue that igored the resource key, thanks @ddctd143 for pointing out

@ghost
Copy link

ghost commented Dec 9, 2015

@boukeversteegh Your implementation does not allow to use custom data key, because in your overriding methods $resourceKey isn't used at all.

At example, if you need to specify data key, but you need to use a custom key, then this implementation should work

    public function collection($resourceKey, array $data)
    {
        return $resourceKey === false ? $data : [$resourceKey => $data];
    }

    public function item($resourceKey, array $data)
    {
        return $resourceKey === false ? $data : [$resourceKey => $data];
    }

@boukeversteegh
Copy link

oh you are right, that was a pretty bad mistake. let me correct. i would like this behavior:

  • false results in no wrapper array at all
  • null results in 'data'
  • anything else results in anything else

let me rewrite
edit: fixed!

@boukeversteegh
Copy link

I am working on a PR to implement optional, yet customizable resource keys (wrapper arrays) but I'm having trouble understanding the intention behind DataArraySerializer and ArraySerializer. Hopefully @localheinz or @philsturgeon can help out.

From the unit tests, it seems that DataArraySerializer is ignoring the resourceKey on purpose. I.e: even though the transformer sets the resourceKey to "author", the array is returned as {"author": {"data": {"name": "Dave"}}}. If I add support for overriding the data key with the resourceKey, the unit tests break, and it would break backwards compatibility with codebases that use resourceKeys, but expect them to be ignored.

Otherwise I could add this feature to ArraySerializer, but that one does not wrap items. In my opinion neither serializers make much sense. The ArraySerializer uses the resourceKey only for collections, and does not allow disabling. The DataSerializer also wraps single items, but does not allow overriding, nor disabling the data key.

IMO all of this functionality could and should be supported by a single Serializer such as the one I proposed above.

Now the question is should we support this flexibility by breaking compatibility, or by adding a new serializer, and if so what should it be called.

ResourceArraySerializer
NamedArraySerializer
FlexArraySerializer
DefaultArraySerializer
ResourceKeyArraySerializer

What do you think? I will PR soon if we can come up with a nice solution.

@aleemb
Copy link

aleemb commented Dec 22, 2015

@boukeversteegh Maybe call it PlainOldSerializer in spirit of the POJO/POCO convention and matching user expectations.

@aftabnaveed
Copy link

@boukeversteegh did you create a PR for that already, and has it been merged?

@ryanrca
Copy link

ryanrca commented Aug 11, 2017

New to fractal, and ran into this today. Our data structures nest a lot of includes. My front end dev is not happy with having to write object keys like this:

var genres = music.data.productable.data.decorators.data.genres;

It gets pretty long winded.
We would be a lot happier if it were more concise:

var genres = music.data.productable.decorators.genres;

Status of this PR?

@philsturgeon
Copy link
Member

philsturgeon commented Aug 11, 2017 via email

@michaeldnelson
Copy link

michaeldnelson commented Jan 28, 2018

I will +1 this I will also be writing a custom serializer to make my api more readable. Is nesting object attributes in a data property a standard I should be aware of or is this just a quirk of the library?

edit: Also thanks for this library, hoping to use it regularly now that I am back on php.

@philsturgeon
Copy link
Member

philsturgeon commented Jan 28, 2018 via email

@cristenicu
Copy link

To wrap the response with data I used an ArraySerializer and Dingo's morphed event

protected $listen = [
    'Dingo\Api\Event\ResponseWasMorphed' => [
    	'App\Listeners\FormatDingoResponse'
    ]
];
class FormatDingoResponse
{
    public function handle(\Dingo\Api\Event\ResponseWasMorphed $event)
    {
        $data = isset($event->content['data']) ? $event->content['data'] : $event->content;
        unset($data['meta']);

        $meta = isset($event->content['meta']) ? $event->content['meta'] : [];

        $event->content = ['data' => $data, 'meta' => $meta];
    }
}

@youmax210139
Copy link

better to overwrite the serializer within dingo

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        if ($adapter = app('api.transformer')->getAdapter()) {
            $manager = $adapter->getFractal();
            $manager->setSerializer(new \App\Transformers\Serializer\DataArraySerializer());
        }
    }
}

@mrFANRA
Copy link

mrFANRA commented Oct 26, 2023

Based on @mediaholding I am using the following solution. Notice that by default, when $resourceKey === null the serializer will create data, as per original implementation.

PHP8 changed types support, now it is not working, In new version need add false var type in function. Temporary make like this =(

class MyArraySerializer extends ArraySerializer
{
    const NOWRAP = 'nowrap';

    public function collection($resourceKey, array $data): array
    {
        // null cheking for remove wrapper in root
        if ($resourceKey == self::NOWRAP || is_null(resourceKey)) {
            return $data;
        }
        return array($resourceKey ?: 'data' => $data);
    }

    public function item($resourceKey, array $data): array
    {
        // null cheking for remove wrapper in root
        if ($resourceKey == self::NOWRAP || is_null(resourceKey)) {
            return $data;
        }
        return array($resourceKey ?: 'data' => $data);
    }
}

Usage:

class FriendshipTransformer extends TransformerAbstract
{
    protected $availableIncludes = [
        'user',
    ];

    public function transform(Friendship $friendship)
    {
        return $friendship->toArray();
    }

    public function includeUser(Friendship $friendship)
    {
        // pass false as resourceKey
        return $this->item($friendship->user, new UserTransformer, MyArraySerializer::NOWRAP);
    }
}

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