/fractal

Handle the output of complex data structures ready for JSON output

Primary LanguagePHPMIT LicenseMIT

Fractal

Build Status Coverage Status Total Downloads Latest Stable Version

When building an API it is common for people to just grab stuff from the database and pass it to json_encode(). This might be passable for "trivial" API's but if they are in use by the public, or used by an iPhone application then this will quickly lead to inconsistent output.

This package aims to do a few simple things:

  • Allow an area for you to type-cast your data, so not all of your booleans look like "0"
  • Avoid db schema changes changing your output
  • Allow for simple, flexible and controllable embedding of data, avoiding infinite loops

This package is compliant with PSR-0, PSR-1, and PSR-2. If you notice compliance oversights, please send a patch via pull request.

Install

Via Composer

{
    "require": {
        "league/fractal": "0.6.*"
    }
}

Usage

Shove this in your base controller or an IoC somehow.

use League\Fractal;

// Create a top level instance somewhere
$fractal = new Fractal\Manager();
$fractal->setRequestedScopes(explode(',', $_GET['embed']));

Creating Resources and Transformers

In your controllers you can then create "resources", of which there are three types:

  • League\Fractal\Resource\Item - A singular resource, probably one entry in a data store
  • League\Fractal\Resource\Collection - A collection of resources

The Item and Collection constructors will take any kind of data you wish to send it as the first argument, and then a "transformer" as the second argument. This can be callable or a string containing a fully-qualified class name.

The transformer will the raw data passed back into it, so if you pass an instance of BookModel into an ItemResource then you can expect this instance to be BookModel. If you passed an array or a collection (an object implementing ArrayIterator) of BookModel instances then this transform method will be run on each of those instances.

use League\Fractal;

$resource = new Fractal\Resource\Collection($books, function(BookModel $book) {
    return [
        'id' => (int) $book->id,
        'title' => $book->title,
        'year' => $book->yr,
    ];
});

If you want to reuse your transformers (recommended) then create classes somewhere and pass in the name. Assuming you use an autoloader of course. These classes must extend League\Fractal\TransformerAbstract and contain a transform method, much like the callback example: public function transform(Foo $foo).

use League\Fractal;
use Acme\Transformer\BookTransformer;

$resource = new Fractal\Resource\Item($book, new BookTransformer);
$resource = new Fractal\Resource\Collection($books, new BookTransformer);

Embedding Data

Your transformer at this point is mainly just giving you a method to handle array conversion from you data source (or whatever your model is returning) to a simple array. Embedding data in an intelligent way can be tricky as data can have all sorts of relationships. Many developers try to find a perfect balance between not making too many HTTP requests and not downloading more data than they need to, so flexibility is also important.

Sticking with the book example, the BookTransformer might contain an optional embed for an author.

<?php namespace App\Transformer;

use Book;
use League\Fractal\TransformerAbstract;

class BookTransformer extends TransformerAbstract
{
    /**
     * List of resources possible to embed via this transformer
     *
     * @var array
     */
    protected $availableEmbeds = [
        'author'
    ];

    /**
     * Turn this item object into a generic array
     *
     * @return array
     */
    public function transform(Book $book)
    {
        return [
            'id'    => (int) $book->id,
            'title' => $book->title,
            'year'  => $book->yr,
        ];
    }

    /**
     * Embed Author
     *
     * @return League\Fractal\ItemResource
     */
    public function embedAuthor(Book $book)
    {
        $author = $book->author;

        return $this->item($author, new AuthorTransformer);
    }
}

So if a client application were to call the URL /books?embed=author then they would see author data in the response. These can be nested with dot notation, as far as you like.

E.g: /books?embed=author,publishers,publishers.somethingelse

This example happens to be using the lazy-loading functionality of an ORM for $book->author, but there is no reason that eager-loading could not also be used by inspecting the $_GET['embed'] list of requested scopes. This would just be a translation array, turning scopes into eager-loading requirements.

Outputting Processed Data

When ready to output this data, you must convert the "resource" back into data. Calling $fractal->createData(); with a resource argument will run the transformers (any any nested transformer calls) and convert everything to an array for you to output:

// Play with the array
$data = $fractal->createData($resource)->toArray();

// Straight to JSON
$json = $fractal->createData($resource)->toJson();

If you want to use something other than JSON then you'll need to think that one up yourself. If you're using horribly complicated XML for example, then you will probably need to create some specific view files, which negates the purpose of using this system entirely. Auto-generated XML, YAML or anything similar could easily be set up in a switch, just check against the Accept header.

Pagination

When working with a large data set it obviously makes sense to offer pagination options to the endpoint, otherwise that data can get very slow. To avoid writing your own pagination output into every endpoint you can utilize the the League\Fractal\Resource\Collection::setPaginator() method.

The paginator passed to setPaginator() must implement League\Fractal\Pagination\PaginatorInterface and it's specified methods.

Fractal currently only ships with an adapter for Laravel's illuminate/pagination package as League\Fractal\Pagination\IlluminatePaginatorAdapter.

Inside of Laravel 4, using the Eloquent or Query Builder method paginate(), the following syntax is possible:

use League\Fractal\Resource\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Acme\Model\Book;
use Acme\Transformer\BookTransformer;

$paginator = Books::paginate();
$books = $books->getCollection();

$resource = new Collection($books, new BookTransformer);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));

TODO

This is still in concept stage, and these issues are left to explore:

  • Switch return array to use instance properties in transform()
  • Implement HATEOAS/HAL links
  • Add smart embed syntax, e.g: ?embed=foo:limit(5):order(something,asc)

Testing

$ phpunit

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.