Skip to content

HAL+JSON & HAL+XML API transformer outputting valid (PSR-7) API Responses.

License

Notifications You must be signed in to change notification settings

nilportugues/php-hal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HAL+JSON & HAL+XML API Transformer

Build Status Scrutinizer Code Quality SensioLabsInsight Latest Stable Version Total Downloads License Donate

  1. Installation
  2. Mapping
  3. HAL Serialization
  4. HAL Paginated Resource
  5. PSR-7 Response objects

1. Installation

Use Composer to install the package:

$ composer require nilportugues/hal

2. Mapping

Given a PHP Object, and a series of mappings, the HAL+JSON and HAL+XML API transformer will represent the given data following the https://tools.ietf.org/html/draft-kelly-json-hal-07 specification draft.

For instance, given the following piece of code, defining a Blog Post and some comments:

$post = new Post(
  new PostId(9),
  'Hello World',
  'Your first post',
  new User(
      new UserId(1),
      'Post Author'
  ),
  [
      new Comment(
          new CommentId(1000),
          'Have no fear, sers, your king is safe.',
          new User(new UserId(2), 'Barristan Selmy'),
          [
              'created_at' => (new DateTime('2015/07/18 12:13:00'))->format('c'),
              'accepted_at' => (new DateTime('2015/07/19 00:00:00'))->format('c'),
          ]
      ),
  ]
);

We will have to map all the involved classes. This can be done as one single array, or a series of Mapping classes.

2.1 Mapping with arrays

Mapping involved classes using arrays is done as follows:

use NilPortugues\Api\Mapping\Mapper;

$mappings = [
    [
        'class' => Post::class,
        'alias' => 'Message',
        'aliased_properties' => [
            'author' => 'author',
            'title' => 'headline',
            'content' => 'body',
        ],
        'hide_properties' => [

        ],
        'id_properties' => [
            'postId',
        ],
        'urls' => [
            // Mandatory
            'self' => 'http://example.com/posts/{postId}',
             // Optional
            'comments' => 'http://example.com/posts/{postId}/comments'
        ],
        'curies' => [
            'name' => 'example',
            'href' => "http://example.com/docs/rels/{rel}",
        ]
    ],
    [
        'class' => User::class,
        'alias' => '',
        'aliased_properties' => [],
        'hide_properties' => [],
        'id_properties' => [
            'userId',
        ],
        'urls' => [
            'self' => 'http://example.com/users/{userId}',
        ],
        'curies' => [
            'name' => 'example',
            'href' => "http://example.com/docs/rels/{rel}",
        ]
    ],
    [
        'class' => Comment::class,
        'alias' => '',
        'aliased_properties' => [],
        'hide_properties' => [],
        'id_properties' => [
            'commentId',
        ],
        'urls' => [
            'self' => 'http://example.com/comments/{commentId}',
        ],
        'curies' => [
            'name' => 'example',
            'href' => "http://example.com/docs/rels/{rel}",
        ]
    ]
];

$mapper = new Mapper($mappings);

2.2 Mapping with Mapping class

In order to map with Mapping class, you need to create a new class for each involved class.

This mapping fashion scales way better than using an array.

All Mapping classes will extend the \NilPortugues\Api\Mappings\HalMapping interface.

// PostMapping.php

class PostMapping implements \NilPortugues\Api\Mappings\HalMapping 
{
    public function getClass()
    {
        return Post::class;
    }

    public function getAlias()
    {
        return 'Message';
    }

    public function getAliasedProperties()
    {
        return [
           'author' => 'author',
           'title' => 'headline',
           'content' => 'body',
       ];
    }

    public function getHideProperties()
    {
        return [];
    }

    public function getIdProperties()
    {
        return ['postId'];
    }
   
    public function getUrls()
    {
        return [            
            'self' => 'http://example.com/posts/{postId}', // Mandatory            
            'comments' => 'http://example.com/posts/{postId}/comments' // Optional
        ];
    }

    public function getCuries()
    {
        return [
           'name' => 'example',
           'href' => "http://example.com/docs/rels/{rel}",
        ];
    }
}


// UserMapping.php

class UserMapping implements \NilPortugues\Api\Mappings\HalMapping 
{
    public function getClass()
    {
        return User::class;
    }

    public function getAlias()
    {
        return '';
    }

    public function getAliasedProperties()
    {
        return [];
    }

    public function getHideProperties()
    {
        return [];
    }

    public function getIdProperties()
    {
        return ['postId'];
    }
   
    public function getUrls()
    {
        return [            
            'self' => 'http://example.com/users/{userId}'
        ];
    }

    public function getCuries()
    {
        return [
           'name' => 'example',
           'href' => "http://example.com/docs/rels/{rel}",
        ];
    }
}


// CommentMapping.php

class CommentMapping implements \NilPortugues\Api\Mappings\HalMapping 
{
    public function getClass()
    {
        return Comment::class;
    }

    public function getAlias()
    {
        return '';
    }

    public function getAliasedProperties()
    {
        return [];
    }

    public function getHideProperties()
    {
        return [];
    }

    public function getIdProperties()
    {
        return ['commentId'];
    }
   
    public function getUrls()
    {
        return [            
            'self' => 'http://example.com/comments/{commentId}',
        ];
    }

    public function getCuries()
    {
        return [
           'name' => 'example',
           'href' => "http://example.com/docs/rels/{rel}",
        ];
    }
} 

$mappings = [
    PostMapping::class,
    UserMapping::class,
    CommentMapping::class,
];
$mapper = new Mapper($mappings);

3. HAL Serialization

Calling the transformer will output a valid HAL response using the correct formatting:

use NilPortugues\Api\Hal\JsonTransformer; 
use NilPortugues\Api\Hal\HalSerializer;
use NilPortugues\Api\Hal\Http\Message\Response;

$transformer = new JsonTransformer($mapper);
//For XML: $transformer = new XmlTransformer($mapper);

//Output transformation
$serializer = new HalSerializer($transformer);
$output = $serializer->serialize($post);

//PSR7 Response with headers and content.
$response = new Response($output);

header(
    sprintf(
        'HTTP/%s %s %s',
        $response->getProtocolVersion(),
        $response->getStatusCode(),
        $response->getReasonPhrase()
    )
);

foreach($response->getHeaders() as $header => $values) {
    header(sprintf("%s:%s\n", $header, implode(', ', $values)));
}

echo $response->getBody();

3.1 HAL+JSON

HTTP/1.1 200 OK
Cache-Control: private, max-age=0, must-revalidate
Content-type: application/hal+json

Output:

{
    "post_id": 9,
    "headline": "Hello World",
    "body": "Your first post",
    "_embedded": {
        "author": {
            "user_id": 1,
            "name": "Post Author",
            "_links": {
                "self": {
                    "href": "http://example.com/users/1"
                },
                "example:friends": {
                    "href": "http://example.com/users/1/friends"
                },
                "example:comments": {
                    "href": "http://example.com/users/1/comments"
                }
            }
        },
        "comments": [
            {
                "comment_id": 1000,
                "dates": {
                    "created_at": "2015-08-13T22:47:45+02:00",
                    "accepted_at": "2015-08-13T23:22:45+02:00"
                },
                "comment": "Have no fear, sers, your king is safe.",
                "_embedded": {
                    "user": {
                        "user_id": 2,
                        "name": "Barristan Selmy",
                        "_links": {
                            "self": {
                                "href": "http://example.com/users/2"
                            },
                            "example:friends": {
                                "href": "http://example.com/users/2/friends"
                            },
                            "example:comments": {
                                "href": "http://example.com/users/2/comments"
                            }
                        }
                    }
                },
                "_links": {
                    "example:user": {
                        "href": "http://example.com/users/2"
                    },
                    "self": {
                        "href": "http://example.com/comments/1000"
                    }
                }
            }
        ]
    },
    "_links": {
        "curies": [
            {
                "name": "example",
                "href": "http://example.com/docs/rels/{rel}",
                "templated": true
            }
        ],
        "self": {
            "href": "http://example.com/posts/9"
        },
        "example:author": {
            "href": "http://example.com/users/1"
        },
        "example:comments": {
            "href": "http://example.com/posts/9/comments"
        }
    }
}

3.2 HAL+XML

For XML output use the sample code but using the XML transformer instead:

$transformer = new XmlTransformer($mapper);

Output:

HTTP/1.1 200 OK
Cache-Control: private, max-age=0, must-revalidate
Content-type: application/hal+xml
<?xml version="1.0" encoding="UTF-8"?>
<resource href="http://example.com/posts/9">
  <post_id><![CDATA[9]]></post_id>
  <headline><![CDATA[Hello World]]></headline>
  <body><![CDATA[Your first post]]></body>
  <embedded>
    <resource href="http://example.com/users/1" rel="author">
      <user_id><![CDATA[1]]></user_id>
      <name><![CDATA[Post Author]]></name>
      <links>
        <link rel="self" href="http://example.com/users/1"/>
        <link rel="example:friends" href="http://example.com/users/1/friends"/>
        <link rel="example:comments" href="http://example.com/users/1/comments"/>
      </links>
    </resource>
    <comments>
      <resource href="http://example.com/comments/1000">
        <comment_id><![CDATA[1000]]></comment_id>
        <dates>
          <created_at><![CDATA[2015-07-18T12:13:00+00:00]]></created_at>
          <accepted_at><![CDATA[2015-07-19T00:00:00+00:00]]></accepted_at>
        </dates>
        <comment><![CDATA[Have no fear, sers, your king is safe.]]></comment>
        <embedded>
          <resource href="http://example.com/users/2" rel="user">
            <user_id><![CDATA[2]]></user_id>
            <name><![CDATA[Barristan Selmy]]></name>
            <links>
              <link rel="self" href="http://example.com/users/2"/>
              <link rel="example:friends" href="http://example.com/users/2/friends"/>
              <link rel="example:comments" href="http://example.com/users/2/comments"/>
            </links>
          </resource>
        </embedded>
        <links>
          <link rel="example:user" href="http://example.com/users/2"/>
          <link rel="self" href="http://example.com/comments/1000"/>
        </links>
      </resource>
    </comments>
  </embedded>
  <links>
    <curies>
      <link rel="resource" href="http://example.com/docs/rels/{rel}">
        <name><![CDATA[example]]></name>
        <templated><![CDATA[true]]></templated>
      </link>
    </curies>
    <link rel="self" href="http://example.com/posts/9"/>
    <link rel="example:author" href="http://example.com/users/1"/>
    <link rel="example:comments" href="http://example.com/posts/9/comments"/>
  </links>
</resource>

4. HAL Paginated Resource

A pagination object to easy the usage of this package is provided.

For both XML and JSON output, use the HalPagination object to build your paginated representation of the current resource.

Methods provided by HalPagination are as follows:

  • setSelf($self)
  • setFirst($first)
  • setPrev($prev)
  • setNext($next)
  • setLast($last)
  • setCount($count)
  • setTotal($total)
  • setEmbedded(array $embedded)

In order to use it, create a new HalPagination instance, use the setters and pass the instance to the serialize($value) method of the serializer.

Everything else will be handled by serializer itself. Easy as that!

use NilPortugues\Api\Hal\HalPagination; 
use NilPortugues\Api\Hal\HalSerializer; 
use NilPortugues\Api\Hal\JsonTransformer; 

// ...
//$objects is an array of objects, such as Post::class.
// ...
 
$page = new HalPagination();

//set the amounts
$page->setTotal(20);
$page->setCount(10);

//set the objects
$page->setEmbedded($objects);

//set up the pagination links
$page->setSelf('/post?page=1');
$page->setPrev('/post?page=1');
$page->setFirst('/post?page=1');
$page->setLast('/post?page=1');

$output = $serializer->serialize($page);

5. Response objects

The following PSR-7 Response objects providing the right headers and HTTP status codes are available.

Its use is optional and are provided as a starting point.

  • NilPortugues\Api\Hal\Http\Message\ErrorResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourceCreatedResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourceDeletedResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourceNotFoundResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourcePatchErrorResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourcePostErrorResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourceProcessingResponse($body)
  • NilPortugues\Api\Hal\Http\Message\ResourceUpdatedResponse($body)
  • NilPortugues\Api\Hal\Http\Message\Response($body)
  • NilPortugues\Api\Hal\Http\Message\UnsupportedActionResponse($body)

Quality

To run the PHPUnit tests at the command line, go to the tests directory and issue phpunit.

This library attempts to comply with PSR-1, PSR-2, PSR-4 and PSR-7.

If you notice compliance oversights, please send a patch via Pull Request.

Contribute

Contributions to the package are always welcome!

Support

Get in touch with me using one of the following means:

Authors

License

The code base is licensed under the MIT license.