Compare commits

...

3 Commits

Author SHA1 Message Date
Maxime Thévenot 9413202660 WIP : test authentication for api 2018-09-06 08:20:27 +02:00
Maxime Thévenot 215e325a8c Feat : configure and test api on add 2018-09-06 08:20:09 +02:00
Maxime Thévenot 9f8682c896 Feat : Install hateoas bundle 2018-09-06 08:19:16 +02:00
176 changed files with 12021 additions and 15 deletions
+1
View File
@@ -56,6 +56,7 @@ class AppKernel extends Kernel
new JMS\SerializerBundle\JMSSerializerBundle(),
new FOS\RestBundle\FOSRestBundle(),
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(),
];
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
@@ -238,12 +238,17 @@ fos_rest:
view:
formats: { json: true, xml: false, rss: false }
view_response_listener: true
mime_types:
json: ['application/json', 'application/json;version=1.0', 'application/json;version=2.0']
serializer:
serialize_null: true
format_listener:
rules:
- { path: '^/api/', priorities: ['json'], fallback_format: 'json' }
param_fetcher_listener: true
# versioning:
# enabled: true
# regex: '/(v|version)=(?P<version>[0-9\.]+)/'
# sensio_framework_extra:
# view: { annotations: false }
+10 -1
View File
@@ -5,12 +5,19 @@ security:
fos_userbundle:
#id: fos_user.user_provider.username_email
id: app.user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true
simple_preauth:
authenticator: app.api_key_authenticator
provider: fos_userbundle
main:
pattern: ^/
remember_me:
@@ -33,6 +40,8 @@ security:
anonymous: true
switch_user:
provider: fos_userbundle
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
@@ -79,3 +79,6 @@ services:
app.user_provider:
class: AppBundle\Security\Core\User\FOSUBUserProvider
arguments: ["@fos_user.user_manager","@doctrine.orm.entity_manager","@mt.slug","@service_container", { facebook: facebookId, linkedin: linkedinId}]
app.api_key_authenticator:
class: AppBundle\Security\ApiKeyAuthenticator
+2 -1
View File
@@ -55,7 +55,8 @@
"jms/serializer-bundle": "^2.4",
"friendsofsymfony/rest-bundle": "^2.4",
"symfony/twig-bundle": "^4.1",
"white-october/pagerfanta-bundle": "^1.2"
"white-october/pagerfanta-bundle": "^1.2",
"willdurand/hateoas-bundle": "^1.4"
},
"require-dev": {
"sensio/generator-bundle": "^3.0",
+119 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "452758fcee42c8c80f3c08ed2a0d3361",
"content-hash": "fbdda65eb230886b1e84516b1efe5b55",
"packages": [
{
"name": "alcaeus/mongo-php-adapter",
@@ -6090,6 +6090,124 @@
],
"time": "2018-06-20T09:32:42+00:00"
},
{
"name": "willdurand/hateoas",
"version": "2.12.0",
"source": {
"type": "git",
"url": "https://github.com/willdurand/Hateoas.git",
"reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willdurand/Hateoas/zipball/71b1af62b398dc9a870ac0b16c84bdc23a76a5c5",
"reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5",
"shasum": ""
},
"require": {
"doctrine/annotations": "~1.0",
"doctrine/common": "~2.0",
"jms/metadata": "~1.1",
"jms/serializer": "^1.7",
"php": "^5.5|^7.0",
"phpoption/phpoption": ">=1.1.0,<2.0-dev",
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0"
},
"require-dev": {
"pagerfanta/pagerfanta": "~1.0",
"phpunit/phpunit": "~5",
"symfony/dependency-injection": "~2.4 || ~3.0 || ~4.0",
"symfony/routing": "~2.4 || ~3.0 || ~4.0",
"symfony/yaml": "~2.4 || ~3.0 || ~4.0",
"twig/twig": "~1.12"
},
"suggest": {
"symfony/routing": "To use the SymfonyRouteFactory.",
"symfony/yaml": "To use yaml based configuration.",
"twig/twig": "To use the Twig extensions."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.12-dev"
}
},
"autoload": {
"psr-0": {
"Hateoas": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Adrien Brault",
"email": "adrien.brault@gmail.com"
},
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
}
],
"description": "A PHP library to support implementing representations for HATEOAS REST web services",
"time": "2018-02-23T17:05:31+00:00"
},
{
"name": "willdurand/hateoas-bundle",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/willdurand/BazingaHateoasBundle.git",
"reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa",
"reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa",
"shasum": ""
},
"require": {
"jms/serializer-bundle": "~1.0 || ^2.0",
"php": ">5.4 |^7.0",
"symfony/framework-bundle": "~2.3 || ~3.0 || ~4.0",
"willdurand/hateoas": "^2.10.0"
},
"require-dev": {
"phpunit/phpunit": "~4.5 || ~5.0",
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0",
"symfony/stopwatch": "~2.4 || ~3.0 || ~4.0",
"twig/twig": "~1.12"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"Bazinga\\Bundle\\HateoasBundle\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
}
],
"description": "Integration of Hateoas into Symfony2.",
"keywords": [
"HATEOAS",
"rest"
],
"time": "2018-01-27T13:03:07+00:00"
},
{
"name": "willdurand/jsonp-callback-validator",
"version": "v1.1.0",
@@ -13,16 +13,21 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Hateoas\Configuration\Annotation as Hateoas;
use AppBundle\Representation\SingleAd;
class AdController extends FOSRestController
{
/**
* @Rest\Get(path="/api/ads/{id}", name="api_ad_show", requirements={"id"="\d+"})
* @Rest\View
*
*/
public function showAction(Ad $ad)
{
return $ad;
// return $ad;
return new SingleAd($ad);
}
/**
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* Ad
@@ -19,6 +20,22 @@ use JMS\Serializer\Annotation\Expose;
* @ORM\HasLifecycleCallbacks()
*
* @ExclusionPolicy("all")
*
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "api_ad_show",
* parameters = { "id" = "expr(object.getId())" },
* absolute=true
* )
* )
*
* @Hateoas\Relation(
* "company",
* embedded = @Hateoas\Embedded("expr(object.getCompany())")
* )
*
*
*/
class Ad
{
@@ -9,6 +9,8 @@ use AppBundle\Utils\Traits\TagsJsonTrait;
use AppBundle\Utils\Traits\TimestampableTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
/**
* Company
@@ -16,6 +18,8 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
* @ORM\Table(name="company")
* @ORM\Entity(repositoryClass="AppBundle\Repository\CompanyRepository")
* @ORM\HasLifecycleCallbacks()
*
* @ExclusionPolicy("all")
*/
class Company
{
@@ -32,6 +36,7 @@ class Company
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @Expose
*/
private $id;
@@ -39,6 +44,7 @@ class Company
* @var string
*
* @ORM\Column(name="brand_name", type="string", length=255, options={"comment":"Nom commercial"}, nullable=true)
* @Expose
*/
private $brandName;
@@ -67,6 +73,7 @@ class Company
* @var string
*
* @ORM\Column(name="logo", type="string", nullable=true)
* @Expose
*/
private $logo;
@@ -6,10 +6,15 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
* @ORM\Table(name="fos_user")
*
* @ExclusionPolicy("all")
*/
class User extends BaseUser
{
@@ -24,6 +29,7 @@ class User extends BaseUser
* @var string
*
* @ORM\Column(name="firstname", type="string", length=255, nullable=true)
* @Expose
*/
private $firstname;
@@ -31,6 +37,7 @@ class User extends BaseUser
* @var string
*
* @ORM\Column(name="lastname", type="string", length=255, nullable=true)
* @Expose
*/
private $lastname;
@@ -38,6 +45,7 @@ class User extends BaseUser
* @var \DateTime
*
* @ORM\Column(name="birthday", type="datetime", length=255, nullable=true)
* @Expose
*/
private $birthday;
@@ -52,6 +60,7 @@ class User extends BaseUser
* @var string
*
* @ORM\Column(name="gender", type="string", length=255, nullable=true)
* @Expose
*/
private $gender;
@@ -2,9 +2,16 @@
namespace AppBundle\Representation;
use Pagerfanta\Pagerfanta;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation\Type;
use Pagerfanta\Pagerfanta;
/**
* @Hateoas\Relation(
* "authenticated_user",
* embedded = @Hateoas\Embedded("expr(service('security.token_storage').getToken().getUser())")
* )
*/
class Ads
{
/**
@@ -12,28 +19,28 @@ class Ads
*/
public $data;
public $meta;
public function __construct(Pagerfanta $data)
{
$this->data = $data;
$this->addMeta('limit', $data->getMaxPerPage());
$this->addMeta('current_items', count($data->getCurrentPageResults()));
$this->addMeta('total_items', $data->getNbResults());
$this->addMeta('offset', $data->getCurrentPageOffsetStart());
}
public function addMeta($name, $value)
{
if (isset($this->meta[$name])) {
throw new \LogicException(sprintf('This meta already exists. You are trying to override this meta, use the setMeta method instead for the %s meta.', $name));
}
$this->setMeta($name, $value);
}
public function setMeta($name, $value)
{
$this->meta[$name] = $value;
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace AppBundle\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation\Type;
use AppBundle\Entity\Ad;
/**
* @Hateoas\Relation(
* "authenticated_user",
* embedded = @Hateoas\Embedded("expr(service('security.token_storage').getToken().getUser())")
* )
*/
class SingleAd
{
/**
* @Type("AppBundle\Entity\Ad")
*/
public $data;
public function __construct(Ad $data)
{
$this->data = $data;
}
}
@@ -0,0 +1,64 @@
<?php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$bearer = $request->headers->get('Authorization');
if (!$bearer) {
throw new BadCredentialsException();
}
return new PreAuthenticatedToken(
'anon.',
$bearer,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$accessToken = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($accessToken);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$accessToken,
$providerKey,
$user->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response("Authentication Failed :(", 401);
}
}
@@ -0,0 +1,47 @@
<?php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
// NOT USED AT THE MOMENT
class ApiKeyUserProvider implements UserProviderInterface
{
public function getUsernameForApiKey($apiKey)
{
// Look up the username based on the token in the database, via
// an API call, or do something entirely different
// $username = ...;
return $username;
}
public function loadUserByUsername($username)
{
return new User(
$username,
null,
// the roles for the user - you may choose to determine
// these dynamically somehow based on the user
array('ROLE_API')
);
}
public function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public function supportsClass($class)
{
return User::class === $class;
}
}
@@ -227,4 +227,11 @@ class FOSUBUserProvider extends BaseFOSUBProvider
$user->$setter($response->getAccessToken());
return $user;
}
public function getUsernameForApiKey($apiKey)
{
$username = 'clementNv';
return $username;
}
}
+2 -2
View File
@@ -10,12 +10,12 @@ return array(
'06dd8487319ccd8403765f5b8c9f2d61' => $vendorDir . '/alcaeus/mongo-php-adapter/lib/Mongo/functions.php',
'92c8763cd6170fce6fcfe7e26b4e8c10' => $vendorDir . '/symfony/phpunit-bridge/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'ddc0a4d7e61c0286f0f8593b1903e894' => $vendorDir . '/clue/stream-filter/src/functions.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
'5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php',
'32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php',
@@ -21,6 +21,7 @@ return array(
'JMS\\Serializer' => array($vendorDir . '/jms/serializer/src'),
'JMS\\' => array($vendorDir . '/jms/parser-lib/src'),
'Imagine' => array($vendorDir . '/imagine/imagine/lib'),
'Hateoas' => array($vendorDir . '/willdurand/hateoas/src'),
'FFMpeg' => array($vendorDir . '/php-ffmpeg/php-ffmpeg/src'),
'Evenement' => array($vendorDir . '/evenement/evenement/src'),
'Doctrine\\ODM\\MongoDB' => array($vendorDir . '/doctrine/mongodb-odm/lib'),
@@ -85,6 +85,7 @@ return array(
'Doctrine\\Bundle\\DoctrineBundle\\' => array($vendorDir . '/doctrine/doctrine-bundle'),
'Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'),
'Clue\\StreamFilter\\' => array($vendorDir . '/clue/stream-filter/src'),
'Bazinga\\Bundle\\HateoasBundle\\' => array($vendorDir . '/willdurand/hateoas-bundle'),
'Alcaeus\\MongoDbAdapter\\' => array($vendorDir . '/alcaeus/mongo-php-adapter/lib/Alcaeus/MongoDbAdapter'),
'' => array($baseDir . '/src'),
);
+17 -2
View File
@@ -11,12 +11,12 @@ class ComposerStaticInit1b32d02bd8fea2d524bf79b36ada5830
'06dd8487319ccd8403765f5b8c9f2d61' => __DIR__ . '/..' . '/alcaeus/mongo-php-adapter/lib/Mongo/functions.php',
'92c8763cd6170fce6fcfe7e26b4e8c10' => __DIR__ . '/..' . '/symfony/phpunit-bridge/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
'ddc0a4d7e61c0286f0f8593b1903e894' => __DIR__ . '/..' . '/clue/stream-filter/src/functions.php',
'8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php',
'2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php',
'5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php',
'32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php',
@@ -157,6 +157,10 @@ class ComposerStaticInit1b32d02bd8fea2d524bf79b36ada5830
'Composer\\CaBundle\\' => 18,
'Clue\\StreamFilter\\' => 18,
),
'B' =>
array (
'Bazinga\\Bundle\\HateoasBundle\\' => 29,
),
'A' =>
array (
'Alcaeus\\MongoDbAdapter\\' => 23,
@@ -486,6 +490,10 @@ class ComposerStaticInit1b32d02bd8fea2d524bf79b36ada5830
array (
0 => __DIR__ . '/..' . '/clue/stream-filter/src',
),
'Bazinga\\Bundle\\HateoasBundle\\' =>
array (
0 => __DIR__ . '/..' . '/willdurand/hateoas-bundle',
),
'Alcaeus\\MongoDbAdapter\\' =>
array (
0 => __DIR__ . '/..' . '/alcaeus/mongo-php-adapter/lib/Alcaeus/MongoDbAdapter',
@@ -581,6 +589,13 @@ class ComposerStaticInit1b32d02bd8fea2d524bf79b36ada5830
0 => __DIR__ . '/..' . '/imagine/imagine/lib',
),
),
'H' =>
array (
'Hateoas' =>
array (
0 => __DIR__ . '/..' . '/willdurand/hateoas/src',
),
),
'F' =>
array (
'FFMpeg' =>
+122
View File
@@ -6478,6 +6478,128 @@
"paging"
]
},
{
"name": "willdurand/hateoas",
"version": "2.12.0",
"version_normalized": "2.12.0.0",
"source": {
"type": "git",
"url": "https://github.com/willdurand/Hateoas.git",
"reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willdurand/Hateoas/zipball/71b1af62b398dc9a870ac0b16c84bdc23a76a5c5",
"reference": "71b1af62b398dc9a870ac0b16c84bdc23a76a5c5",
"shasum": ""
},
"require": {
"doctrine/annotations": "~1.0",
"doctrine/common": "~2.0",
"jms/metadata": "~1.1",
"jms/serializer": "^1.7",
"php": "^5.5|^7.0",
"phpoption/phpoption": ">=1.1.0,<2.0-dev",
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0"
},
"require-dev": {
"pagerfanta/pagerfanta": "~1.0",
"phpunit/phpunit": "~5",
"symfony/dependency-injection": "~2.4 || ~3.0 || ~4.0",
"symfony/routing": "~2.4 || ~3.0 || ~4.0",
"symfony/yaml": "~2.4 || ~3.0 || ~4.0",
"twig/twig": "~1.12"
},
"suggest": {
"symfony/routing": "To use the SymfonyRouteFactory.",
"symfony/yaml": "To use yaml based configuration.",
"twig/twig": "To use the Twig extensions."
},
"time": "2018-02-23T17:05:31+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.12-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-0": {
"Hateoas": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Adrien Brault",
"email": "adrien.brault@gmail.com"
},
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
}
],
"description": "A PHP library to support implementing representations for HATEOAS REST web services"
},
{
"name": "willdurand/hateoas-bundle",
"version": "1.4.0",
"version_normalized": "1.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/willdurand/BazingaHateoasBundle.git",
"reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa",
"reference": "d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa",
"shasum": ""
},
"require": {
"jms/serializer-bundle": "~1.0 || ^2.0",
"php": ">5.4 |^7.0",
"symfony/framework-bundle": "~2.3 || ~3.0 || ~4.0",
"willdurand/hateoas": "^2.10.0"
},
"require-dev": {
"phpunit/phpunit": "~4.5 || ~5.0",
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0",
"symfony/stopwatch": "~2.4 || ~3.0 || ~4.0",
"twig/twig": "~1.12"
},
"time": "2018-01-27T13:03:07+00:00",
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Bazinga\\Bundle\\HateoasBundle\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
}
],
"description": "Integration of Hateoas into Symfony2.",
"keywords": [
"HATEOAS",
"rest"
]
},
{
"name": "willdurand/jsonp-callback-validator",
"version": "v1.1.0",
@@ -107,6 +107,8 @@ final class Versions
'twig/twig' => 'v2.5.0@6a5f676b77a90823c2d4eaf76137b771adf31323',
'vimeo/vimeo-api' => '1.3.0@46820e2d777362849137c1eb14f5cdb657c289ea',
'white-october/pagerfanta-bundle' => 'v1.2.1@3f97e71c9d3f75e7a6179aa82c863497bf298b2d',
'willdurand/hateoas' => '2.12.0@71b1af62b398dc9a870ac0b16c84bdc23a76a5c5',
'willdurand/hateoas-bundle' => '1.4.0@d1f915fd4f8a7cd43a88a0ce97ffb28abe3a94fa',
'willdurand/jsonp-callback-validator' => 'v1.1.0@1a7d388bb521959e612ef50c5c7b1691b097e909',
'willdurand/negotiation' => 'v2.3.1@03436ededa67c6e83b9b12defac15384cb399dc9',
'zendframework/zend-code' => '3.3.1@c21db169075c6ec4b342149f446e7b7b724f95eb',
@@ -0,0 +1,2 @@
vendor/
composer.lock
@@ -0,0 +1,54 @@
language: php
sudo: false
git:
depth: 1
cache:
directories:
- $HOME/.composer/cache
php:
- 5.5
- 5.6
- 7.0
- 7.1
- hhvm
matrix:
allow_failures:
- php: hhvm
matrix:
exclude:
- php: 5.5
env: SYMFONY_VERSION=4.0.*
- php: 5.6
env: SYMFONY_VERSION=4.0.*
- php: 7.0
env: SYMFONY_VERSION=4.0.*
- php: hhvm
env: SYMFONY_VERSION=4.0.*
env:
- SYMFONY_VERSION=2.3.*
- SYMFONY_VERSION=2.4.*
- SYMFONY_VERSION=2.8.*
- SYMFONY_VERSION=3.0.*
- SYMFONY_VERSION=3.2.*
- SYMFONY_VERSION=2.3.* DEPS=low
- SYMFONY_VERSION=2.4.* DEPS=low
- SYMFONY_VERSION=2.8.* DEPS=low
- SYMFONY_VERSION=3.0.* DEPS=low
- SYMFONY_VERSION=3.2.* DEPS=low
- SYMFONY_VERSION=4.0.*
before_script:
- if [ "$DEPS" = 'low' ] ; then COMPOSER_PARAMS="--prefer-lowest --prefer-stable" ; fi
- composer self-update
- composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update
- composer update $COMPOSER_PARAMS
script:
- ./vendor/bin/phpunit --coverage-text
@@ -0,0 +1,34 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle;
use Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler\ExpressionFunctionPass;
use Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler\ExtensionDriverPass;
use Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler\RelationProviderPass;
use Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler\UrlGeneratorPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class BazingaHateoasBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new UrlGeneratorPass());
$container->addCompilerPass(new ExpressionFunctionPass());
$container->addCompilerPass(new RelationProviderPass());
$container->addCompilerPass(new ExtensionDriverPass());
}
}
@@ -0,0 +1,30 @@
Contributing
============
First of all, **thank you** for contributing, **you are awesome**!
Here are a few rules to follow in order to ease code reviews, and discussions before
maintainers accept and merge your work.
You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
[PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).
You MUST run the test suite.
You MUST write (or update) unit tests.
You SHOULD write documentation.
Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request.
One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
Thank you!
@@ -0,0 +1,70 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Config\FileLocator;
/**
* @author William Durand <william.durand1@gmail.com>
*/
class BazingaHateoasExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$config = $this->processConfiguration(new Configuration(), $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
foreach (array('serializer', 'configuration', 'generator', 'helper', 'twig') as $file) {
if ('twig' === $file && false === $config['twig_extension']['enabled']) {
continue;
}
$loader->load($file . '.xml');
}
// Based on JMSSerializerBundle
if ('none' === $config['metadata']['cache']) {
$container->removeAlias('hateoas.configuration.metadata.cache');
} elseif ('file' === $config['metadata']['cache']) {
$container
->getDefinition('hateoas.configuration.metadata.cache.file_cache')
->replaceArgument(0, $config['metadata']['file_cache']['dir']);
$dir = $container->getParameterBag()->resolveValue($config['metadata']['file_cache']['dir']);
if (!file_exists($dir)) {
if (!$rs = @mkdir($dir, 0777, true)) {
throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $dir));
}
}
} else {
$container->setAlias(
'hateoas.configuration.metadata.cache',
new Alias($config['metadata']['cache'], false)
);
}
$container
->getDefinition('hateoas.event_subscriber.json')
->setPublic(true)
->replaceArgument(0, new Reference($config['serializer']['json']));
$container
->getDefinition('hateoas.event_subscriber.xml')
->setPublic(true)
->replaceArgument(0, new Reference($config['serializer']['xml']));
}
}
@@ -0,0 +1,54 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class ExpressionFunctionPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$expressionEvaluator = $container->getDefinition('hateoas.expression.evaluator');
foreach ($container->findTaggedServiceIds('hateoas.expression_function') as $id => $tags) {
$functionDefinition = $container->getDefinition($id);
if (!$this->implementsExpressionFunctionInterface($container, $functionDefinition)) {
throw new InvalidArgumentException(
sprintf(
'Service %s tagged with hateoas.expression_function must implement %s',
$id,
'Hateoas\Expression\ExpressionFunctionInterface'
)
);
}
$expressionEvaluator->addMethodCall('registerFunctionId', array($id));
}
}
private function implementsExpressionFunctionInterface(ContainerBuilder $container, Definition $definition)
{
$class = $container->getParameterBag()->resolveValue($definition->getClass());
$refClass = new \ReflectionClass($class);
return $refClass->implementsInterface('Hateoas\Expression\ExpressionFunctionInterface');
}
}
@@ -0,0 +1,51 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
class ExtensionDriverPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$extensionDriver = $container->getDefinition('hateoas.configuration.metadata.extension_driver');
foreach ($container->findTaggedServiceIds('hateoas.configuration_extension') as $id => $tags) {
$extensionDefinition = $container->getDefinition($id);
if (!$this->implementsConfigurationExtensionInterface($container, $extensionDefinition)) {
throw new InvalidArgumentException(
sprintf(
'Service %s tagged with hateoas.configuration_extension must implement %s',
$id,
'Hateoas\Configuration\Metadata\ConfigurationExtensionInterface'
)
);
}
$extensionDriver->addMethodCall('registerExtension', [$extensionDefinition]);
}
}
private function implementsConfigurationExtensionInterface(ContainerBuilder $container, Definition $definition)
{
$class = $container->getParameterBag()->resolveValue($definition->getClass());
$refClass = new \ReflectionClass($class);
return $refClass->implementsInterface('Hateoas\Configuration\Metadata\ConfigurationExtensionInterface');
}
}
@@ -0,0 +1,53 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/**
* @author Daniel Leech <daniel@dantleech.com>
*/
class RelationProviderPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$registryDefinition = $container->getDefinition('hateoas.configuration.provider.resolver.chain');
$relationProviderDefinitions = array();
foreach (array_keys($container->findTaggedServiceIds('hateoas.relation_provider')) as $id) {
$definition = $container->getDefinition($id);
$this->assertProvider($container, $definition);
$relationProviderDefinitions[] = $definition;
}
$registryDefinition->replaceArgument(0, $relationProviderDefinitions);
}
private function assertProvider(ContainerBuilder $container, Definition $definition)
{
$class = $container->getParameterBag()->resolveValue($definition->getClass());
$refClass = new \ReflectionClass($class);
if (!$refClass->implementsInterface('Hateoas\Configuration\Provider\Resolver\RelationProviderResolverInterface')) {
throw new InvalidArgumentException(sprintf(
'Relation provider "%s" does not implement the ReleationProviderResolver interface',
$definition->getClass()
));
}
}
}
@@ -0,0 +1,59 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class UrlGeneratorPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$registryDefinition = $container->getDefinition('hateoas.generator.registry');
foreach ($container->findTaggedServiceIds('hateoas.url_generator') as $id => $attributes) {
$name = !empty($attributes[0]['alias']) ? $attributes[0]['alias'] : $id;
if ($this->isSymfonyUrlGenerator($container, $container->getDefinition($id))) {
$definition = new Definition(
'Hateoas\UrlGenerator\SymfonyUrlGenerator',
array(new Reference($id))
);
$definition->setPublic(false);
$id = 'hateoas.generator.user.' . $name;
$container->setDefinition($id, $definition);
}
$registryDefinition->addMethodCall(
'set',
array($name, new Reference($id))
);
}
}
private function isSymfonyUrlGenerator(ContainerBuilder $container, Definition $definition)
{
$class = $container->getParameterBag()->resolveValue($definition->getClass());
$refClass = new \ReflectionClass($class);
return $refClass->implementsInterface('Symfony\Component\Routing\Generator\UrlGeneratorInterface');
}
}
@@ -0,0 +1,60 @@
<?php
/**
* This file is part of the HateoasBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Bazinga\Bundle\HateoasBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* @author William Durand <william.durand1@gmail.com>
*/
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree builder.
*
* @return TreeBuilder The tree builder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('bazinga_hateoas');
$rootNode
->children()
->arrayNode('metadata')
->addDefaultsIfNotSet()
->children()
->scalarNode('cache')->defaultValue('file')->end()
->arrayNode('file_cache')
->addDefaultsIfNotSet()
->children()
->scalarNode('dir')->defaultValue('%kernel.cache_dir%/hateoas')->end()
->end()
->end()
->end()
->end()
->arrayNode('serializer')
->addDefaultsIfNotSet()
->children()
->scalarNode('json')->defaultValue('hateoas.serializer.json_hal')->end()
->scalarNode('xml')->defaultValue('hateoas.serializer.xml')->end()
->end()
->end()
->arrayNode('twig_extension')
->addDefaultsIfNotSet()
->canBeDisabled()
->end()
;
return $treeBuilder;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;
/**
* Adds some function to the default ExpressionLanguage.
*
* To get a service, use service('request').
* To get a parameter, use parameter('kernel.debug').
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionLanguage extends BaseExpressionLanguage
{
protected function registerFunctions()
{
parent::registerFunctions();
$this->register('service', function ($arg) {
return sprintf('$this->get(%s)', $arg);
}, function (array $variables, $value) {
return $variables['container']->get($value);
});
$this->register('parameter', function ($arg) {
return sprintf('$this->getParameter(%s)', $arg);
}, function (array $variables, $value) {
return $variables['container']->getParameter($value);
});
$this->register('is_granted', function ($attribute, $object = null) {
return sprintf('call_user_func_array(array($this->get(security.authorization_checker), isGranted), array(%s, %s))', $attribute, $object);
}, function (array $variables, $attribute, $object = null) {
return call_user_func_array(
array($variables['container']->get('security.authorization_checker'), 'isGranted'),
[$attribute, $object]
);
});
}
}
@@ -0,0 +1,58 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Hateoas\Expression;
use Hateoas\Expression\ExpressionEvaluator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
class LazyFunctionExpressionEvaluator extends ExpressionEvaluator
{
/**
* @var ContainerInterface
*/
private $container;
private $functionsIds = array();
public function __construct(
ExpressionLanguage $expressionLanguage,
array $context = array(),
ContainerInterface $container
) {
parent::__construct($expressionLanguage, $context);
$this->container = $container;
}
public function evaluate($expression, $data)
{
if (!empty($this->functionsIds)) {
$this->registerFunctions();
}
return parent::evaluate($expression, $data);
}
public function evaluateArray(array $array, $data)
{
if (!empty($this->functionsIds)) {
$this->registerFunctions();
}
return parent::evaluateArray($array, $data);
}
public function registerFunctionId($serviceId)
{
$this->functionsIds[] = $serviceId;
}
private function registerFunctions()
{
foreach ($this->functionsIds as $serviceId) {
$this->registerFunction($this->container->get($serviceId));
}
$this->functionsIds = array();
}
}
@@ -0,0 +1,19 @@
Copyright (c) 2017 William Durand <william.durand1@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,41 @@
BazingaHateoasBundle
====================
[![Build Status](https://secure.travis-ci.org/willdurand/BazingaHateoasBundle.png)](http://travis-ci.org/willdurand/BazingaHateoasBundle)
This bundle integrates [Hateoas](http://github.com/willdurand/Hateoas) with
[Symfony](http://symfony.com).
Documentation
-------------
For documentation, see:
Resources/doc/
[Read the documentation](https://github.com/willdurand/BazingaHateoasBundle/blob/master/Resources/doc/index.md)
Contributing
------------
See
[CONTRIBUTING](https://github.com/willdurand/BazingaHateoasBundle/blob/master/CONTRIBUTING.md)
file.
Credits
-------
* William Durand <william.durand1@gmail.com>
* [All contributors](https://github.com/willdurand/BazingaHateoasBundle/contributors)
License
-------
This bundle is released under the MIT license. See the complete license in the
bundle:
Resources/meta/LICENSE
@@ -0,0 +1,87 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="hateoas.configuration.provider.resolver.chain.class">Hateoas\Configuration\Provider\Resolver\ChainResolver</parameter>
<parameter key="hateoas.configuration.provider.resolver.method.class">Hateoas\Configuration\Provider\Resolver\MethodResolver</parameter>
<parameter key="hateoas.configuration.provider.resolver.static_method.class">Hateoas\Configuration\Provider\Resolver\StaticMethodResolver</parameter>
<parameter key="hateoas.configuration.provider.resolver.symfony_container.class">Hateoas\Configuration\Provider\Resolver\SymfonyContainerResolver</parameter>
<parameter key="hateoas.configuration.relation_provider.class">Hateoas\Configuration\Provider\RelationProvider</parameter>
<parameter key="hateoas.configuration.relations_repository.class">Hateoas\Configuration\RelationsRepository</parameter>
<parameter key="hateoas.configuration.metadata.yaml_driver.class">Hateoas\Configuration\Metadata\Driver\YamlDriver</parameter>
<parameter key="hateoas.configuration.metadata.xml_driver.class">Hateoas\Configuration\Metadata\Driver\XmlDriver</parameter>
<parameter key="hateoas.configuration.metadata.annotation_driver.class">Hateoas\Configuration\Metadata\Driver\AnnotationDriver</parameter>
<parameter key="hateoas.configuration.metadata.extension_driver.class">Hateoas\Configuration\Metadata\Driver\ExtensionDriver</parameter>
</parameters>
<services>
<service id="hateoas.configuration.provider.resolver.chain" class="%hateoas.configuration.provider.resolver.chain.class%" public="false">
<argument type="collection"></argument>
</service>
<service id="hateoas.configuration.provider.resolver.method" class="%hateoas.configuration.provider.resolver.method.class%" public="false">
<tag name="hateoas.relation_provider" />
</service>
<service id="hateoas.configuration.provider.resolver.static_method" class="%hateoas.configuration.provider.resolver.static_method.class%" public="false">
<tag name="hateoas.relation_provider" />
</service>
<service id="hateoas.configuration.provider.resolver.symfony_container" class="%hateoas.configuration.provider.resolver.symfony_container.class%" public="false">
<argument type="service" id="service_container"/>
<tag name="hateoas.relation_provider" />
</service>
<service id="hateoas.configuration.provider.resolver" alias="hateoas.configuration.provider.resolver.chain"/>
<service id="hateoas.configuration.relation_provider" class="%hateoas.configuration.relation_provider.class%" public="false">
<argument type="service" id="hateoas.configuration.metadata_factory" />
<argument type="service" id="hateoas.configuration.provider.resolver" />
</service>
<service id="hateoas.configuration.relations_repository" class="%hateoas.configuration.relations_repository.class%" public="false">
<argument type="service" id="hateoas.configuration.metadata_factory" />
<argument type="service" id="hateoas.configuration.relation_provider" />
</service>
<service id="hateoas.configuration.metadata.yaml_driver" class="%hateoas.configuration.metadata.yaml_driver.class%" public="false">
<argument type="service" id="jms_serializer.metadata.file_locator" />
</service>
<service id="hateoas.configuration.metadata.xml_driver" class="%hateoas.configuration.metadata.xml_driver.class%" public="false">
<argument type="service" id="jms_serializer.metadata.file_locator" />
</service>
<service id="hateoas.configuration.metadata.annotation_driver" class="%hateoas.configuration.metadata.annotation_driver.class%" public="false">
<argument type="service" id="annotation_reader" />
</service>
<service id="hateoas.configuration.metadata.extension_driver" class="%hateoas.configuration.metadata.extension_driver.class%" public="false">
<argument type="service" id="hateoas.configuration.metadata.annotation_driver" />
</service>
<service id="hateoas.configuration.metadata.chain_driver" class="%jms_serializer.metadata.chain_driver.class%" public="false">
<argument type="collection">
<argument type="service" id="hateoas.configuration.metadata.yaml_driver" />
<argument type="service" id="hateoas.configuration.metadata.xml_driver" />
<argument type="service" id="hateoas.configuration.metadata.extension_driver" />
</argument>
</service>
<service id="hateoas.configuration.metadata.cache.file_cache" class="%jms_serializer.metadata.cache.file_cache.class%" public="false">
<argument></argument> <!-- Directory -->
</service>
<service id="hateoas.configuration.metadata.cache" alias="hateoas.configuration.metadata.cache.file_cache" public="false"></service>
<service id="hateoas.configuration.metadata_factory" class="%jms_serializer.metadata.metadata_factory.class%" public="false">
<argument type="service" id="hateoas.configuration.metadata.chain_driver" />
<argument>Metadata\ClassHierarchyMetadata</argument>
<argument>%kernel.debug%</argument>
<call method="setCache">
<argument type="service" id="hateoas.configuration.metadata.cache" on-invalid="ignore" />
</call>
</service>
</services>
</container>
@@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="hateoas.generator.registry.class">Hateoas\UrlGenerator\UrlGeneratorRegistry</parameter>
<parameter key="hateoas.generator.symfony.class">Hateoas\UrlGenerator\SymfonyUrlGenerator</parameter>
</parameters>
<services>
<service id="hateoas.generator.registry" class="%hateoas.generator.registry.class%" public="true">
<argument type="service" id="hateoas.generator.symfony" />
</service>
<service id="hateoas.generator.symfony" class="%hateoas.generator.symfony.class%">
<argument type="service" id="router" />
</service>
</services>
</container>
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="hateoas.helper.link.class">Hateoas\Helper\LinkHelper</parameter>
</parameters>
<services>
<service id="hateoas.helper.link" class="%hateoas.helper.link.class%" public="true">
<argument type="service" id="hateoas.link_factory" />
<argument type="service" id="hateoas.configuration.relations_repository" />
</service>
</services>
</container>
@@ -0,0 +1,105 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<!-- Factories -->
<parameter key="hateoas.link_factory.class">Hateoas\Factory\LinkFactory</parameter>
<parameter key="hateoas.links_factory.class">Hateoas\Factory\LinksFactory</parameter>
<parameter key="hateoas.embeds_factory.class">Hateoas\Factory\EmbeddedsFactory</parameter>
<!-- Expressions -->
<parameter key="hateoas.expression.evaluator.class">Bazinga\Bundle\HateoasBundle\Hateoas\Expression\LazyFunctionExpressionEvaluator</parameter>
<parameter key="bazinga_hateoas.expression_language.class">Bazinga\Bundle\HateoasBundle\ExpressionLanguage\ExpressionLanguage</parameter>
<parameter key="hateoas.expression.link.class">Hateoas\Expression\LinkExpressionFunction</parameter>
<!-- Serializers & Handlers -->
<parameter key="hateoas.serializer.xml.class">Hateoas\Serializer\XmlSerializer</parameter>
<parameter key="hateoas.serializer.json_hal.class">Hateoas\Serializer\JsonHalSerializer</parameter>
<parameter key="hateoas.serializer.exclusion_manager.class">Hateoas\Serializer\ExclusionManager</parameter>
<!-- Subscribers -->
<parameter key="hateoas.event_subscriber.xml.class">Hateoas\Serializer\EventSubscriber\XmlEventSubscriber</parameter>
<parameter key="hateoas.event_subscriber.json.class">Hateoas\Serializer\EventSubscriber\JsonEventSubscriber</parameter>
<!-- Inline deferrers -->
<parameter key="hateoas.inline_deferrer.embeds.class">Hateoas\Serializer\Metadata\InlineDeferrer</parameter>
<parameter key="hateoas.inline_deferrer.links.class">Hateoas\Serializer\Metadata\InlineDeferrer</parameter>
</parameters>
<services>
<!-- Factories -->
<service id="hateoas.link_factory" class="%hateoas.link_factory.class%" public="false">
<argument type="service" id="hateoas.expression.evaluator" />
<argument type="service" id="hateoas.generator.registry" />
</service>
<service id="hateoas.links_factory" class="%hateoas.links_factory.class%" public="false">
<argument type="service" id="hateoas.configuration.relations_repository" />
<argument type="service" id="hateoas.link_factory" />
<argument type="service" id="hateoas.serializer.exclusion_manager" />
</service>
<service id="hateoas.embeds_factory" class="%hateoas.embeds_factory.class%" public="false">
<argument type="service" id="hateoas.configuration.relations_repository" />
<argument type="service" id="hateoas.expression.evaluator" />
<argument type="service" id="hateoas.serializer.exclusion_manager" />
</service>
<!-- Expressions -->
<service id="hateoas.expression.evaluator" class="%hateoas.expression.evaluator.class%">
<argument type="service" id="bazinga_hateoas.expression_language" />
<argument type="collection"/>
<argument type="service" id="service_container" />
<call method="setContextVariable">
<argument>container</argument>
<argument type="service" id="service_container"/>
</call>
</service>
<service id="bazinga_hateoas.expression_language" class="%bazinga_hateoas.expression_language.class%" />
<service id="hateoas.expression.link" class="%hateoas.expression.link.class%" public="true">
<argument type="service" id="hateoas.helper.link" />
<tag name="hateoas.expression_function"/>
</service>
<!-- Serializers & Handlers -->
<service id="hateoas.serializer.xml" class="%hateoas.serializer.xml.class%" public="false">
<call method="setMetadataFactory">
<argument type="service" id="jms_serializer.metadata_factory" />
</call>
</service>
<service id="hateoas.serializer.json_hal" class="%hateoas.serializer.json_hal.class%" public="false" />
<service id="hateoas.serializer.exclusion_manager" class="%hateoas.serializer.exclusion_manager.class%">
<argument type="service" id="hateoas.expression.evaluator" />
</service>
<!-- Subscribers -->
<service id="hateoas.event_subscriber.xml" class="%hateoas.event_subscriber.xml.class%">
<tag name="jms_serializer.event_subscriber" />
<argument /> <!-- xml serializer -->
<argument type="service" id="hateoas.links_factory" />
<argument type="service" id="hateoas.embeds_factory" />
</service>
<service id="hateoas.event_subscriber.json" class="%hateoas.event_subscriber.json.class%">
<tag name="jms_serializer.event_subscriber" />
<argument /> <!-- json serializer -->
<argument type="service" id="hateoas.links_factory" />
<argument type="service" id="hateoas.embeds_factory" />
<argument type="service" id="hateoas.inline_deferrer.embeds" />
<argument type="service" id="hateoas.inline_deferrer.links" />
</service>
<service id="hateoas.inline_deferrer.embeds" class="%hateoas.inline_deferrer.embeds.class%" public="false">
<argument type="service" id="jms_serializer.metadata_factory"/>
</service>
<service id="hateoas.inline_deferrer.links" class="%hateoas.inline_deferrer.links.class%" public="false">
<argument type="service" id="jms_serializer.metadata_factory"/>
</service>
</services>
</container>
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="hateoas.twig.link.class">Hateoas\Twig\Extension\LinkExtension</parameter>
</parameters>
<services>
<service id="hateoas.twig.link" class="%hateoas.twig.link.class%">
<argument type="service" id="hateoas.helper.link" />
<tag name="twig.extension" />
</service>
</services>
</container>
@@ -0,0 +1,260 @@
BazingaHateoasBundle
====================
This bundle integrates [Hateoas](http://github.com/willdurand/Hateoas) into
[Symfony](http://symfony.com).
Installation
------------
### Step 1: Download the Bundle
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
```bash
$ composer require willdurand/hateoas-bundle
```
This command requires you to have Composer installed globally, as explained
in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.
### Step 2: Enable the Bundle
Then, enable the bundle by adding the following line in the `app/AppKernel.php`
file of your project:
```php
<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(),
);
// ...
}
}
```
> **Note:**
> The bundle requires the [JMSSerializerBundle](http://jmsyst.com/bundles/JMSSerializerBundle) to be
> registered. If you haven't done that already, you should register it in the kernel aswell:
>
> ```php
> // app/AppKernel.php
>
> // ...
> public function registerBundles()
> {
> $bundles = array(
> // ...
> new JMS\SerializerBundle\JMSSerializerBundle(),
> );
>
> // ...
> }
> ```
Usage
-----
### Mapping Objects
Refer to the [Hateoas documentation](http://github.com/willdurand/Hateoas) to
find out how to map your objects.
### Serializing objects
The BazingaHateoasBundle transparently hooks into the JMS serializer, there
are no special considerations:
````php
// My/Controller.php
class SomeController extends Controller
{
public function resourceAction(Request $request)
{
$post = $repository->find('BlogBundle:post');
$json = $this->container->get('jms_serializer')->serialize($post, 'json');
return new Response($json, 200, array('Content-Type' => 'application/json'));
}
}
````
Expression Language
-------------------
This bundle provides three extra functions to the expression language:
## `is_granted`
Allows you to exclude certain routes by checking whether the currently authenticated user
has certain permissions or not. For example:
```php
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* "delete",
* href = @Hateoas\Route(
* "post_delete",
* parameters = { "id" = "expr(object.getId())" }
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(not is_granted(['ROLE_ADMIN']))"
* )
* )
*/
class Post
{
// ...
}
```
If the authenticated user has the `ROLE_ADMIN` role the route will be exposed, otherwise
the route will be excluded.
## `parameter`
Allows you to fetch a parameter from the service container:
```
/**
* @Hateoas\Relation(
* "delete",
* href = @Hateoas\Route(
* "post_delete",
* parameters = { "foo" = "expr(parameter('foo'))" }
* )
* )
*/
class Post
{
// ...
}
```
## `service`
Allows you to fetch a service from the service container.
Extending
---------
### RelationProviderResolver
A relation provider resolver is a class which provides a PHP callable. This
callable will provide relations (links) for a given object.
You can add new relation providers by implementing the
`Hateoas\Configuration\Provider\Resolver\RelationProviderResolverInterface`
interface and adding a definition to the dependency injection configuration
with the tag `hateoas.relation_provider`:
````xml
<?xml version="1.0" ?>
<container ...>
<!-- ... -->
<services>
<!-- ... -->
<service id="acme_foo.hateoas.relation_provider_resolver.foobar" class="Acme\FooBundle\Hateoas\RelationProviderResolver\Foobar">
<tag name="hateoas.relation_provider" />
</service>
</services>
</container>
````
### Configuration Extension
A configuration extension allows you to override already configured relations.
You can add an extension configuration by adding a definition to the dependency
injection configuration with the tag `hateoas.configuration_extension`:
```xml
<?xml version="1.0" ?>
<container ...>
<!-- ... -->
<services>
<!-- ... -->
<service id="acme_foo.hateoas.configuration_extension.foobar" class="Acme\FooBundle\Hateoas\ConfigurationExtension\AcmeFooConfigurationExtension">
<tag name="hateoas.configuration_extension" />
</service>
</services>
</container>
```
```php
<?php
namespace Acme\FooBundle\Hateoas\ConfigurationExtension;
use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use Hateoas\Configuration\Relation;
class AcmeFooConfigurationExtension implements ConfigurationExtensionInterface
{
/**
* {@inheritDoc}
*/
public function decorate(ClassMetadataInterface $classMetadata)
{
if (0 === strpos('Acme\Foo\Model', $classMetadata->getName())) {
// Add a "root" relation to all classes in the `Acme\Foo\Model` namespace
$classMetadata->addRelation(
new Relation(
'root',
'/'
)
);
}
}
}
```
Reference Configuration
-----------------------
``` yaml
# app/config/config*.yml
bazinga_hateoas:
metadata:
cache: file
file_cache:
dir: %kernel.cache_dir%/hateoas
serializer:
json: hateoas.serializer.json_hal
xml: hateoas.serializer.xml
twig_extension:
enabled: true
```
Testing
-------
Setup the test suite using [Composer](http://getcomposer.org/):
$ composer install --dev
Run it using PHPUnit:
$ phpunit
@@ -0,0 +1,271 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Tests\DependencyInjection;
use Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle;
use Bazinga\Bundle\HateoasBundle\DependencyInjection\Compiler\UrlGeneratorPass;
use Bazinga\Bundle\HateoasBundle\Tests\TestCase;
use Bazinga\Bundle\HateoasBundle\Tests\Fixtures\SimpleObject;
use Doctrine\Common\Annotations\AnnotationReader;
use JMS\SerializerBundle\JMSSerializerBundle;
use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Stopwatch\Stopwatch;
class BazingaHateoasExtensionTest extends TestCase
{
public function setUp()
{
$this->clearTempDir();
}
public function tearDown()
{
$this->clearTempDir();
}
public function testLoad()
{
$container = $this->getContainerForConfig(array(array()));
$container->compile();
$serializer = $container->get('jms_serializer');
$this->assertEquals(
json_encode(array(
'name' => 'hello',
'_links' => array(
'all' => array(
'href' => 'http://somewhere/simple-objects',
'foo' => 'bar',
),
'all_2' => array(
'href' => 'http://somewhere/simple-objects',
),
)
)),
$serializer->serialize(new SimpleObject('hello'), 'json')
);
}
public function testRelationProviderPassInvalidProvider()
{
$container = $this->getContainerForConfig(array(array()));
$container->compile();
$definition = $container->getDefinition('hateoas.configuration.provider.resolver.chain');
$arguments = $definition->getArguments();
$this->assertCount(1, $arguments);
$this->assertCount(3, $arguments[0]);
}
/**
* @expectedException InvalidArgumentException
*/
public function testRelationProviderPass()
{
$container = $this->getContainerForConfig(array(array()));
$definition = $container->register('invalid_relation_provider', 'stdClass');
$definition->addTag('hateoas.relation_provider');
$container->compile();
}
public function testLoadUrlGenerator()
{
$container = $this->getContainerForConfig(array(array()));
$urlGeneratorClass = 'Bazinga\Bundle\HateoasBundle\Tests\Fixtures\UrlGenerator';
$container->setParameter('url_generator_2.class', $urlGeneratorClass);
$this->registerUrlGenerator($container, 'url_generator_1', $urlGeneratorClass);
$this->registerUrlGenerator($container, 'url_generator_2', '%url_generator_2.class%');
$container->compile();
$urlGeneratorRegistry = $container->get('hateoas.generator.registry');
$urlGenerator1 = $urlGeneratorRegistry->get('url_generator_1');
$this->assertInstanceOf($urlGeneratorClass, $urlGenerator1);
$urlGenerator2 = $urlGeneratorRegistry->get('url_generator_2');
$this->assertInstanceOf($urlGeneratorClass, $urlGenerator2);
}
public function testLoadSerializer()
{
$class = 'Bazinga\Bundle\HateoasBundle\Tests\Fixtures\JsonSerializer';
$container = $this->getContainerForConfig(array('bazinga_hateoas' => array('serializer' => array('json' => 'custom_serializer'))));
$container->setDefinition('custom_serializer', new Definition($class));
$container->compile();
$jsonListener = $container->get('hateoas.event_subscriber.json');
$reflClass = new \ReflectionClass($jsonListener);
$reflProp = $reflClass->getProperty('jsonSerializer');
$reflProp->setAccessible(true);
$this->assertInstanceOf($class, $reflProp->getValue($jsonListener));
$xmlListener = $container->get('hateoas.event_subscriber.xml');
$reflClass = new \ReflectionClass($xmlListener);
$reflProp = $reflClass->getProperty('xmlSerializer');
$reflProp->setAccessible(true);
$this->assertInstanceOf('Hateoas\Serializer\XmlSerializer', $reflProp->getValue($xmlListener));
}
/**
* @expectedException Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testNotLoadingTwigHelper()
{
$container = $this->getContainerForConfig(array('bazinga_hateoas' => array('twig_extension' => array('enabled' => false))));
$container->findDefinition('hateoas.twig.link');
$container->compile();
}
/**
* @expectedException Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Service invalid_expression_function tagged with hateoas.expression_function must implement Hateoas\Expression\ExpressionFunctionInterface
*/
public function testLoadInvalidExpressionFunction()
{
$container = $this->getContainerForConfig(array(array()));
$container
->setDefinition(
'invalid_expression_function',
new Definition('stdClass')
)
->addTag('hateoas.expression_function')
;
$container->compile();
}
/**
* @expectedException Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Service invalid_configuration_extension tagged with hateoas.configuration_extension must implement Hateoas\Configuration\Metadata\ConfigurationExtensionInterface
*/
public function testLoadInvalidConfigurationExtension()
{
$container = $this->getContainerForConfig(array(array()));
$container
->setDefinition(
'invalid_configuration_extension',
new Definition('stdClass')
)
->addTag('hateoas.configuration_extension')
;
$container->compile();
}
private function clearTempDir()
{
// clear temporary directory
$dir = $this->getTempDir();
if (is_dir($dir)) {
foreach (new \RecursiveDirectoryIterator($dir) as $file) {
$filename = $file->getFileName();
if ('.' === $filename || '..' === $filename) {
continue;
}
@unlink($file->getPathName());
}
@rmdir($dir);
}
}
private function getTempDir()
{
return sys_get_temp_dir() . '/hateoas-bundle';
}
/**
* @see https://github.com/schmittjoh/JMSSerializerBundle/blob/master/Tests/DependencyInjection/JMSSerializerExtensionTest.php
*/
private function getContainerForConfig(array $configs, KernelInterface $kernel = null)
{
if (null === $kernel) {
$kernel = $this->getMock(KernelInterface::class);
$kernel
->expects($this->any())
->method('getBundles')
->will($this->returnValue(array()))
;
}
$router = $this->getMock(UrlGeneratorInterface::class);
$bundles = array(
new BazingaHateoasBundle($kernel),
new JMSSerializerBundle($kernel),
);
$extensions = array_map(function ($bundle) {
return $bundle->getContainerExtension();
}, $bundles);
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', true);
$container->setParameter('kernel.cache_dir', $this->getTempDir());
$container->setParameter('kernel.bundles', array());
$container->set('annotation_reader', new AnnotationReader());
$container->set('router', $router);
$container->set('debug.stopwatch', $this->getMock(Stopwatch::class));
$container->setParameter('foo', 'bar');
foreach ($extensions as $extension) {
$extensionConfig = isset($configs[$extension->getAlias()]) ? $configs[$extension->getAlias()] : array();
$container->registerExtension($extension);
$extension->load(array($extension->getAlias() => $extensionConfig), $container);
}
foreach ($bundles as $bundle) {
$bundle->build($container);
}
$this->setOptimizationPasses($container);
$container->getCompilerPassConfig()->setRemovingPasses(array());
return $container;
}
private function registerUrlGenerator(ContainerBuilder $container, $id, $class)
{
$urlGeneratorDefinition = new Definition($class);
$urlGeneratorDefinition->addTag('hateoas.url_generator');
$container->setDefinition($id, $urlGeneratorDefinition);
}
/**
* @param $container
*/
private function setOptimizationPasses(Container $container)
{
if (class_exists('Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionTemplatesPass')) {
$container->getCompilerPassConfig()->setOptimizationPasses(
array(
new ResolveParameterPlaceHoldersPass(),
new \Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionTemplatesPass(),
new UrlGeneratorPass(),
)
);
} else {
$container->getCompilerPassConfig()->setOptimizationPasses(
array(
new ResolveParameterPlaceHoldersPass(),
new \Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass(),
new UrlGeneratorPass(),
)
);
}
}
}
@@ -0,0 +1,18 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Tests\Fixtures;
use Hateoas\Serializer\JsonSerializerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\SerializationContext;
class JsonSerializer implements JsonSerializerInterface
{
public function serializeLinks(array $links, JsonSerializationVisitor $visitor, SerializationContext $context)
{
}
public function serializeEmbeddeds(array $embeddeds, JsonSerializationVisitor $visitor, SerializationContext $context)
{
}
}
@@ -0,0 +1,37 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Tests\Fixtures;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Type;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* "all",
* href = "http://somewhere/simple-objects",
* attributes = { "foo" = "expr(parameter('foo'))" }
* )
* @Hateoas\Relation(
* "all_2",
* href = "expr(link(object, 'all'))"
* )
*/
class SimpleObject
{
/**
* @Type("string")
* @SerializedName("name")
*/
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
}
@@ -0,0 +1,13 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Tests\Fixtures;
use Hateoas\UrlGenerator\UrlGeneratorInterface;
class UrlGenerator implements UrlGeneratorInterface
{
public function generate($name, array $parameters, $absolute = false)
{
}
}
@@ -0,0 +1,7 @@
<?php
namespace Bazinga\Bundle\HateoasBundle\Tests;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
}
@@ -0,0 +1,17 @@
<?php
use Doctrine\Common\Annotations\AnnotationRegistry;
if (!($loader = @include __DIR__ . '/../vendor/autoload.php')) {
die(<<<EOT
You need to install the project dependencies using Composer:
$ wget http://getcomposer.org/composer.phar
OR
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install --dev
$ phpunit
EOT
);
}
AnnotationRegistry::registerLoader('class_exists');
@@ -0,0 +1,34 @@
{
"name": "willdurand/hateoas-bundle",
"description": "Integration of Hateoas into Symfony2.",
"keywords": [ "rest", "hateoas" ],
"type": "symfony-bundle",
"minimum-stability": "dev",
"license": "MIT",
"authors": [
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
}
],
"require": {
"php": ">5.4 |^7.0",
"symfony/framework-bundle": "~2.3 || ~3.0 || ~4.0",
"willdurand/hateoas": "^2.10.0",
"jms/serializer-bundle": "~1.0 || ^2.0"
},
"require-dev": {
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0",
"twig/twig": "~1.12",
"phpunit/phpunit": "~4.5 || ~5.0",
"symfony/stopwatch": "~2.4 || ~3.0 || ~4.0"
},
"autoload": {
"psr-4": { "Bazinga\\Bundle\\HateoasBundle\\": "" }
},
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
}
}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./Tests/bootstrap.php"
>
<testsuites>
<testsuite name="BazingaHateoasBundle Test Suite">
<directory>./Tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>.</directory>
<exclude>
<directory>Resources</directory>
<directory>Tests</directory>
<directory>vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
@@ -0,0 +1,4 @@
vendor
composer.lock
composer.phar
bin
@@ -0,0 +1,51 @@
language: php
sudo: required
dist: trusty
group: edge
cache:
directories:
- $HOME/.composer/cache
php:
- 5.6
- 7.0
- 7.1
matrix:
exclude:
- php: 5.6
env: SYMFONY_VERSION=4.0.*
- php: 7.0
env: SYMFONY_VERSION=4.0.*
- php: hhvm-3.9
env: SYMFONY_VERSION=4.0.*
- php: hhvm-3.12
env: SYMFONY_VERSION=4.0.*
- php: hhvm-3.15
env: SYMFONY_VERSION=4.0.*
- php: hhvm-3.18
env: SYMFONY_VERSION=4.0.*
fast_finish: true
env:
- SYMFONY_VERSION=2.4.*
- SYMFONY_VERSION=2.5.*
- SYMFONY_VERSION=2.6.*
- SYMFONY_VERSION=2.7.*
- SYMFONY_VERSION=2.8.*
- SYMFONY_VERSION=3.0.*
- SYMFONY_VERSION=3.2.*
- SYMFONY_VERSION=4.0.*
before_install:
- composer self-update
- composer require --no-update symfony/expression-language "$SYMFONY_VERSION"
- composer require --no-update --dev symfony/yaml "$SYMFONY_VERSION"
- composer require --no-update --dev symfony/routing "$SYMFONY_VERSION"
- composer require --no-update --dev symfony/dependency-injection "$SYMFONY_VERSION"
install:
- composer install --no-interaction --prefer-dist
script:
- ./bin/phpunit --coverage-text
@@ -0,0 +1,33 @@
Contributing
============
First of all, **thank you** for contributing, **you are awesome**!
Here are a few rules to follow in order to ease code reviews, and discussions before
maintainers accept and merge your work.
You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
[PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).
You MUST run the test suite.
You MUST write (or update) unit tests.
You SHOULD write documentation.
Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request.
One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
Also, while creating your Pull Request on GitHub, you MUST write a description
which gives the context and/or explains why you are creating it.
Thank you!
@@ -0,0 +1,20 @@
Copyright (c) 2012-2013 William Durand <william.durand1@gmail.com>
Adrien Brault <adrien.brault@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
{
"name": "willdurand/hateoas",
"description": "A PHP library to support implementing representations for HATEOAS REST web services",
"license": "MIT",
"authors": [
{
"name": "William Durand",
"email": "william.durand1@gmail.com"
},
{
"name": "Adrien Brault",
"email": "adrien.brault@gmail.com"
}
],
"require": {
"php": "^5.5|^7.0",
"doctrine/common": "~2.0",
"doctrine/annotations": "~1.0",
"jms/metadata": "~1.1",
"jms/serializer": "^1.7",
"symfony/expression-language": "~2.4 || ~3.0 || ~4.0",
"phpoption/phpoption": ">=1.1.0,<2.0-dev"
},
"require-dev": {
"phpunit/phpunit": "~5",
"symfony/yaml": "~2.4 || ~3.0 || ~4.0",
"symfony/routing": "~2.4 || ~3.0 || ~4.0",
"symfony/dependency-injection": "~2.4 || ~3.0 || ~4.0",
"pagerfanta/pagerfanta": "~1.0",
"twig/twig": "~1.12"
},
"suggest": {
"symfony/yaml": "To use yaml based configuration.",
"symfony/routing": "To use the SymfonyRouteFactory.",
"twig/twig": "To use the Twig extensions."
},
"autoload": {
"psr-0": { "Hateoas": "src/" }
},
"config": {
"bin-dir": "bin"
},
"extra": {
"branch-alias": {
"dev-master": "2.12-dev"
}
}
}
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="https://github.com/willdurand/Hateoas"
xmlns:h="https://github.com/willdurand/Hateoas"
elementFormDefault="qualified">
<annotation>
<documentation><![CDATA[This is the XML Schema for the hateoas serialization]]></documentation>
</annotation>
<complexType name="class">
<sequence>
<element name="relation" type="h:relation" maxOccurs="unbounded"/>
</sequence>
<attribute name="providers" type="normalizedString" use="optional"/>
</complexType>
<complexType name="relation">
<sequence>
<element name="href" type="h:href" minOccurs="0"/>
<element name="embedded" type="h:embedded" minOccurs="0"/>
<element name="exclusion" type="h:exclusion" minOccurs="0" maxOccurs="unbounded"/>
<element name="attribute" type="h:attribute" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<attribute name="rel" type="token" use="required"/>
</complexType>
<complexType name="attribute">
<attribute name="name" type="normalizedString" use="required"/>
<attribute name="value" type="normalizedString" use="required"/>
</complexType>
<complexType name="href">
<sequence>
<element name="parameter" type="h:attribute" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<attribute name="uri" type="normalizedString" use="optional"/>
<attribute name="route" type="normalizedString" use="optional"/>
<attribute name="absolute" type="boolean" use="optional"/>
<attribute name="generator" type="normalizedString" use="optional"/>
</complexType>
<complexType name="embedded">
<sequence>
<element name="content" type="normalizedString"/>
<element name="exclusion" type="h:exclusion" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
<attribute name="xml-element-name" type="token" use="optional"/>
</complexType>
<complexType name="exclusion">
<attribute name="groups" type="normalizedString" use="optional"/>
<attribute name="since-version" type="normalizedString" use="optional"/>
<attribute name="until-version" type="normalizedString" use="optional"/>
<attribute name="max-depth" type="normalizedString" use="optional"/>
<attribute name="exclude-if" type="normalizedString" use="optional"/>
</complexType>
</schema>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="tests/bootstrap.php"
>
<testsuites>
<testsuite name="Hateoas Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>
@@ -0,0 +1,27 @@
<?php
namespace Hateoas\Configuration\Annotation;
/**
* @Annotation
* @Target("ANNOTATION")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Embedded
{
/**
* @Required
*/
public $content;
/**
* @var string
*/
public $xmlElementName;
/**
* @var \Hateoas\Configuration\Annotation\Exclusion
*/
public $exclusion = null;
}
@@ -0,0 +1,41 @@
<?php
namespace Hateoas\Configuration\Annotation;
/**
* @Annotation
* @Target("ANNOTATION")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
final class Exclusion
{
/**
* @var array
*/
public $groups = null;
/**
* @var mixed float/integer
*/
public $sinceVersion = null;
/**
* @var mixed float/integer
*/
public $untilVersion = null;
/**
* If you are working with the FOS/Rest Bundle, be reminded to set
* Rest\View(serializerEnableMaxDepthChecks=true)
* on your controller
*
* @var int
*/
public $maxDepth = null;
/**
* @var string
*/
public $excludeIf = null;
}
@@ -0,0 +1,33 @@
<?php
namespace Hateoas\Configuration\Annotation;
/**
* @Annotation
* @Target("CLASS")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
final class Relation
{
/**
* @Required
*
* @var string
*/
public $name;
public $href = null;
public $embedded = null;
/**
* @var array
*/
public $attributes = array();
/**
* @var \Hateoas\Configuration\Annotation\Exclusion
*/
public $exclusion = null;
}
@@ -0,0 +1,17 @@
<?php
namespace Hateoas\Configuration\Annotation;
/**
* @Annotation
* @Target("CLASS")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class RelationProvider
{
/**
* @var string
*/
public $name;
}
@@ -0,0 +1,28 @@
<?php
namespace Hateoas\Configuration\Annotation;
/**
* @Annotation
* @Target("ANNOTATION")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Route
{
/**
* @Required
*
* @var string
*/
public $name;
/**
* @Required
*/
public $parameters = array();
public $absolute = false;
public $generator = null;
}
@@ -0,0 +1,60 @@
<?php
namespace Hateoas\Configuration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Embedded
{
/**
* @var string|mixed
*/
private $content;
/**
* @var string|null
*/
private $xmlElementName;
/**
* @var Exclusion|null
*/
private $exclusion;
/**
* @param string|mixed $content
* @param string|null $xmlElementName
* @param Exclusion $exclusion
*/
public function __construct($content, $xmlElementName = null, Exclusion $exclusion = null)
{
$this->content = $content;
$this->xmlElementName = $xmlElementName;
$this->exclusion = $exclusion;
}
/**
* @return mixed|string
*/
public function getContent()
{
return $this->content;
}
/**
* @return null|string
*/
public function getXmlElementName()
{
return $this->xmlElementName;
}
/**
* @return Exclusion|null
*/
public function getExclusion()
{
return $this->exclusion;
}
}
@@ -0,0 +1,88 @@
<?php
namespace Hateoas\Configuration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Exclusion
{
/**
* @var null|array
*/
private $groups;
/**
* @var null|float
*/
private $sinceVersion;
/**
* @var null|float
*/
private $untilVersion;
/**
* @var null|integer
*/
private $maxDepth;
/**
* @var null|string
*/
private $excludeIf;
public function __construct(
array $groups = null,
$sinceVersion = null,
$untilVersion = null,
$maxDepth = null,
$excludeIf = null
) {
$this->groups = $groups;
$this->sinceVersion = null !== $sinceVersion ? (float) $sinceVersion : null;
$this->untilVersion = null !== $untilVersion ? (float) $untilVersion : null;
$this->maxDepth = null !== $maxDepth ? (int) $maxDepth : null;
$this->excludeIf = $excludeIf;
}
/**
* @return null|array
*/
public function getGroups()
{
return $this->groups;
}
/**
* @return null|float
*/
public function getSinceVersion()
{
return $this->sinceVersion;
}
/**
* @return null|float
*/
public function getUntilVersion()
{
return $this->untilVersion;
}
/**
* @return int|null
*/
public function getMaxDepth()
{
return $this->maxDepth;
}
/**
* @return null|string
*/
public function getExcludeIf()
{
return $this->excludeIf;
}
}
@@ -0,0 +1,105 @@
<?php
namespace Hateoas\Configuration\Metadata;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\RelationProvider;
use Metadata\MergeableClassMetadata;
use Metadata\MergeableInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class ClassMetadata extends MergeableClassMetadata implements ClassMetadataInterface
{
/**
* @var Relation[]
*/
private $relations = array();
/**
* @var RelationProvider[]
*/
private $relationProviders = array();
/**
* {@inheritDoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritDoc}
*/
public function getRelations()
{
return $this->relations;
}
/**
* {@inheritDoc}
*/
public function getRelationProviders()
{
return $this->relationProviders;
}
/**
* {@inheritDoc}
*/
public function addRelation(Relation $relation)
{
$this->relations[] = $relation;
}
/**
* {@inheritDoc}
*/
public function addRelationProvider(RelationProvider $relationProvider)
{
$this->relationProviders[] = $relationProvider;
}
/**
* {@inheritDoc}
*/
public function merge(MergeableInterface $object)
{
if (!$object instanceof self) {
throw new \InvalidArgumentException(sprintf('Object must be an instance of %s.', __CLASS__));
}
parent::merge($object);
$this->relations = array_merge($this->relations, $object->getRelations());
$this->relationProviders = array_merge($this->relationProviders, $object->getRelationProviders());
}
/**
* {@inheritDoc}
*/
public function serialize()
{
return serialize(array(
$this->relations,
$this->relationProviders,
parent::serialize(),
));
}
/**
* {@inheritDoc}
*/
public function unserialize($str)
{
list(
$this->relations,
$this->relationProviders,
$parentStr
) = unserialize($str);
parent::unserialize($parentStr);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Hateoas\Configuration\Metadata;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\RelationProvider;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
interface ClassMetadataInterface
{
/**
* @return string
*/
public function getName();
/**
* @return Relation[]
*/
public function getRelations();
/**
* @return RelationProvider[]
*/
public function getRelationProviders();
/**
* @param Relation $relation
*/
public function addRelation(Relation $relation);
/**
* @param RelationProvider $relationProvider
*/
public function addRelationProvider(RelationProvider $relationProvider);
}
@@ -0,0 +1,11 @@
<?php
namespace Hateoas\Configuration\Metadata;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
interface ConfigurationExtensionInterface
{
public function decorate(ClassMetadataInterface $classMetadata);
}
@@ -0,0 +1,111 @@
<?php
namespace Hateoas\Configuration\Metadata\Driver;
use Doctrine\Common\Annotations\Reader as AnnotationsReader;
use Hateoas\Configuration\Annotation;
use Hateoas\Configuration\Embedded;
use Hateoas\Configuration\Exclusion;
use Hateoas\Configuration\Metadata\ClassMetadata;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\RelationProvider;
use Hateoas\Configuration\Route;
use Metadata\Driver\DriverInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class AnnotationDriver implements DriverInterface
{
/**
* @var AnnotationsReader
*/
private $reader;
/**
* @param AnnotationsReader $reader
*/
public function __construct(AnnotationsReader $reader)
{
$this->reader = $reader;
}
/**
* {@inheritdoc}
*/
public function loadMetadataForClass(\ReflectionClass $class)
{
$annotations = $this->reader->getClassAnnotations($class);
if (0 === count($annotations)) {
return null;
}
$classMetadata = new ClassMetadata($class->getName());
$classMetadata->fileResources[] = $class->getFilename();
foreach ($annotations as $annotation) {
if ($annotation instanceof Annotation\Relation) {
$classMetadata->addRelation(new Relation(
$annotation->name,
$this->createHref($annotation->href),
$this->createEmbedded($annotation->embedded),
$annotation->attributes ?: array(),
$this->createExclusion($annotation->exclusion)
));
} elseif ($annotation instanceof Annotation\RelationProvider) {
$classMetadata->addRelationProvider(new RelationProvider($annotation->name));
}
}
if (0 === count($classMetadata->getRelations()) && 0 === count($classMetadata->getRelationProviders())) {
return null;
}
return $classMetadata;
}
private function parseExclusion(Annotation\Exclusion $exclusion)
{
return new Exclusion(
$exclusion->groups,
$exclusion->sinceVersion,
$exclusion->untilVersion,
$exclusion->maxDepth,
$exclusion->excludeIf
);
}
private function createHref($href)
{
if ($href instanceof Annotation\Route) {
$href = new Route($href->name, $href->parameters, $href->absolute, $href->generator);
}
return $href;
}
private function createEmbedded($embedded)
{
if ($embedded instanceof Annotation\Embedded) {
$embeddedExclusion = $embedded->exclusion;
if (null !== $embeddedExclusion) {
$embeddedExclusion = $this->parseExclusion($embeddedExclusion);
}
$embedded = new Embedded($embedded->content, $embedded->xmlElementName, $embeddedExclusion);
}
return $embedded;
}
private function createExclusion($exclusion)
{
if (null !== $exclusion) {
$exclusion = $this->parseExclusion($exclusion);
}
return $exclusion;
}
}
@@ -0,0 +1,65 @@
<?php
namespace Hateoas\Configuration\Metadata\Driver;
use Hateoas\Configuration\Metadata\ClassMetadata;
use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
use Metadata\Driver\DriverInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class ExtensionDriver implements DriverInterface
{
/**
* @var DriverInterface
*/
private $delegate;
/**
* @var ConfigurationExtensionInterface[]
*/
private $extensions;
public function __construct(DriverInterface $delegate, array $extensions = [])
{
$this->delegate = $delegate;
$this->extensions = $extensions;
}
/**
* {@inheritdoc}
*/
public function loadMetadataForClass(\ReflectionClass $class)
{
$metadata = $this->delegate->loadMetadataForClass($class);
$newMetadata = false;
if (empty($this->extensions)) {
return $metadata;
}
if (null === $metadata) {
$metadata = new ClassMetadata($class->getName());
$newMetadata = true;
}
foreach ($this->extensions as $extension) {
$extension->decorate($metadata);
}
if ($newMetadata && count($metadata->getRelations()) < 1 && count($metadata->getRelationProviders()) < 1) {
$metadata = null;
}
return $metadata;
}
/**
* @param ConfigurationExtensionInterface $extension
*/
public function registerExtension(ConfigurationExtensionInterface $extension)
{
$this->extensions[] = $extension;
}
}
@@ -0,0 +1,143 @@
<?php
namespace Hateoas\Configuration\Metadata\Driver;
use Hateoas\Configuration\Embedded;
use Hateoas\Configuration\Exclusion;
use Hateoas\Configuration\Metadata\ClassMetadata;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\RelationProvider;
use Hateoas\Configuration\Route;
use JMS\Serializer\Exception\XmlErrorException;
use Metadata\Driver\AbstractFileDriver;
/**
* @author Miha Vrhovnik <miha.vrhovnik@pagein.net>
*/
class XmlDriver extends AbstractFileDriver
{
const NAMESPACE_URI = 'https://github.com/willdurand/Hateoas';
/**
* {@inheritdoc}
*/
protected function loadMetadataFromFile(\ReflectionClass $class, $file)
{
$previous = libxml_use_internal_errors(true);
$root = simplexml_load_file($file);
libxml_use_internal_errors($previous);
if (false === $root) {
throw new XmlErrorException(libxml_get_last_error());
}
$name = $class->getName();
if (!$exists = $root->xpath("./class[@name = '" . $name . "']")) {
throw new \RuntimeException(sprintf('Expected metadata for class %s to be defined in %s.', $name, $file));
}
$classMetadata = new ClassMetadata($name);
$classMetadata->fileResources[] = $file;
$classMetadata->fileResources[] = $class->getFileName();
if ($exists[0]->attributes(self::NAMESPACE_URI)->providers) {
$providers = preg_split('/\s*,\s*/', (string) $exists[0]->attributes(self::NAMESPACE_URI)->providers);
foreach ($providers as $relationProvider) {
$classMetadata->addRelationProvider(new RelationProvider($relationProvider));
}
}
$elements = $exists[0]->children(self::NAMESPACE_URI);
foreach ($elements->relation as $relation) {
$name = (string) $relation->attributes('')->rel;
$href = null;
if (isset($relation->href)) {
$href = $this->createHref($relation->href, $name);
}
$embedded = null;
if (isset($relation->embedded)) {
$embedded = $this->createEmbedded($relation->embedded);
}
$attributes = array();
foreach ($relation->attribute as $attribute) {
$attributes[(string) $attribute->attributes('')->name] = (string) $attribute->attributes('')->value;
}
$exclusion = isset($relation->exclusion) ? $this->parseExclusion($relation->exclusion) : null;
$classMetadata->addRelation(
new Relation(
$name,
$href,
$embedded,
$attributes,
$exclusion
)
);
}
return $classMetadata;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'xml';
}
private function parseExclusion(\SimpleXMLElement $exclusion)
{
return new Exclusion(
isset($exclusion->attributes('')->groups) ? preg_split('/\s*,\s*/', (string) $exclusion->attributes('')->groups) : null,
isset($exclusion->attributes('')->{'since-version'}) ? (string) $exclusion->attributes('')->{'since-version'} : null,
isset($exclusion->attributes('')->{'until-version'}) ? (string) $exclusion->attributes('')->{'until-version'} : null,
isset($exclusion->attributes('')->{'max-depth'}) ? (string) $exclusion->attributes('')->{'max-depth'} : null,
isset($exclusion->attributes('')->{'exclude-if'}) ? (string) $exclusion->attributes('')->{'exclude-if'} : null
);
}
private function createHref($href, $name)
{
if (isset($href->attributes('')->uri) && isset($href->attributes('')->route)) {
throw new \RuntimeException(sprintf(
'uri and route attributes are mutually exclusive, please set only one of them. The problematic relation rel is %s.',
$name
));
} elseif (isset($href->attributes('')->uri)) {
$href = (string) $href->attributes('')->uri;
} else {
$parameters = array();
foreach ($href->parameter as $parameter) {
$parameters[(string) $parameter->attributes('')->name] = (string) $parameter->attributes('')->value;
}
$href = new Route(
(string) $href->attributes('')->route,
$parameters,
null !== ($absolute = $href->attributes('')->absolute) ? 'true' === strtolower($absolute) : false,
isset($href->attributes('')->generator) ? (string) $href->attributes('')->generator : null
);
}
return $href;
}
private function createEmbedded($embedded)
{
$embeddedExclusion = isset($embedded->exclusion) ? $this->parseExclusion($embedded->exclusion) : null;
$xmlElementName = isset($embedded->attributes('')->{'xml-element-name'}) ? (string) $embedded->attributes('')->{'xml-element-name'} : null;
return new Embedded(
(string) $embedded->content,
$xmlElementName,
$embeddedExclusion
);
}
}
@@ -0,0 +1,119 @@
<?php
namespace Hateoas\Configuration\Metadata\Driver;
use Hateoas\Configuration\Embedded;
use Hateoas\Configuration\Exclusion;
use Hateoas\Configuration\Metadata\ClassMetadata;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\RelationProvider;
use Hateoas\Configuration\Route;
use Metadata\Driver\AbstractFileDriver;
use Symfony\Component\Yaml\Yaml;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class YamlDriver extends AbstractFileDriver
{
/**
* {@inheritdoc}
*/
protected function loadMetadataFromFile(\ReflectionClass $class, $file)
{
$config = Yaml::parse(file_get_contents($file));
if (!isset($config[$name = $class->getName()])) {
throw new \RuntimeException(sprintf('Expected metadata for class %s to be defined in %s.', $name, $file));
}
$config = $config[$name];
$classMetadata = new ClassMetadata($name);
$classMetadata->fileResources[] = $file;
$classMetadata->fileResources[] = $class->getFileName();
if (isset($config['relations'])) {
foreach ($config['relations'] as $relation) {
$classMetadata->addRelation(new Relation(
$relation['rel'],
$this->createHref($relation),
$this->createEmbedded($relation),
isset($relation['attributes']) ? $relation['attributes'] : array(),
$this->createExclusion($relation)
));
}
}
if (isset($config['relation_providers'])) {
foreach ($config['relation_providers'] as $relationProvider) {
$classMetadata->addRelationProvider(new RelationProvider($relationProvider));
}
}
return $classMetadata;
}
/**
* {@inheritdoc}
*/
protected function getExtension()
{
return 'yml';
}
private function parseExclusion(array $exclusion)
{
return new Exclusion(
isset($exclusion['groups']) ? $exclusion['groups'] : null,
isset($exclusion['since_version']) ? $exclusion['since_version'] : null,
isset($exclusion['until_version']) ? $exclusion['until_version'] : null,
isset($exclusion['max_depth']) ? $exclusion['max_depth'] : null,
isset($exclusion['exclude_if']) ? $exclusion['exclude_if'] : null
);
}
private function createHref($relation)
{
$href = null;
if (isset($relation['href']) && is_array($href = $relation['href']) && isset($href['route'])) {
$href = new Route(
$href['route'],
isset($href['parameters']) ? $href['parameters'] : array(),
isset($href['absolute']) ? $href['absolute'] : false,
isset($href['generator']) ? $href['generator'] : null
);
}
return $href;
}
private function createEmbedded($relation)
{
$embedded = null;
if (isset($relation['embedded'])) {
$embedded = $relation['embedded'];
if (is_array($embedded)) {
$embeddedExclusion = null;
if (isset($embedded['exclusion'])) {
$embeddedExclusion = $this->parseExclusion($embedded['exclusion']);
}
$xmlElementName = isset($embedded['xmlElementName']) ? $embedded['xmlElementName'] : null;
$embedded = new Embedded($embedded['content'], $xmlElementName, $embeddedExclusion);
}
}
return $embedded;
}
private function createExclusion($relation)
{
$exclusion = null;
if (isset($relation['exclusion'])) {
$exclusion = $this->parseExclusion($relation['exclusion']);
}
return $exclusion;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Hateoas\Configuration\Provider;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use Hateoas\Configuration\Provider\Resolver\RelationProviderResolverInterface;
use Metadata\MetadataFactoryInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class RelationProvider implements RelationProviderInterface
{
/**
* @var MetadataFactoryInterface
*/
private $metadataFactory;
/**
* @var RelationProviderResolverInterface
*/
private $resolver;
public function __construct(MetadataFactoryInterface $metadataFactory, RelationProviderResolverInterface $resolver)
{
$this->metadataFactory = $metadataFactory;
$this->resolver = $resolver;
}
public function getRelations($object)
{
$classMetadata = $this->metadataFactory->getMetadataForClass(get_class($object));
if (!$classMetadata instanceof ClassMetadataInterface) {
return array();
}
$relations = array();
foreach ($classMetadata->getRelationProviders() as $configuration) {
if (null === $relationProviderCallable = $this->resolver->getRelationProvider($configuration, $object)) {
continue;
}
if (!is_callable($relationProviderCallable)) {
throw new \RuntimeException('The returned relation provider is not callable, it should be.');
}
$newRelations = call_user_func_array(
$relationProviderCallable,
array($object, $classMetadata)
);
if (is_array($newRelations)) {
$relations = array_merge($relations, $newRelations);
}
}
return $relations;
}
}
@@ -0,0 +1,15 @@
<?php
namespace Hateoas\Configuration\Provider;
/**
* @author Vyacheslav Salakhutdinov <megazoll@gmail.com>
*/
interface RelationProviderInterface
{
/**
* @param object $object
* @return \Hateoas\Configuration\Relation[] Returns array of Relations for specified object.
*/
public function getRelations($object);
}
@@ -0,0 +1,43 @@
<?php
namespace Hateoas\Configuration\Provider\Resolver;
use Hateoas\Configuration\RelationProvider as RelationProviderConfiguration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class ChainResolver implements RelationProviderResolverInterface
{
/**
* @var RelationProviderResolverInterface[]
*/
private $resolvers;
public function __construct(array $resolvers)
{
$this->resolvers = $resolvers;
}
public function addResolver(RelationProviderResolverInterface $resolver)
{
$this->resolvers[] = $resolver;
}
/**
* {@inheritdoc}
*/
public function getRelationProvider(RelationProviderConfiguration $configuration, $object)
{
$configurationCallable = null;
foreach ($this->resolvers as $resolver) {
$configurationCallable = $resolver->getRelationProvider($configuration, $object);
if (null !== $configurationCallable) {
break;
}
}
return $configurationCallable;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Hateoas\Configuration\Provider\Resolver;
use Hateoas\Configuration\RelationProvider as RelationProviderConfiguration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class MethodResolver implements RelationProviderResolverInterface
{
/**
* {@inheritdoc}
*/
public function getRelationProvider(RelationProviderConfiguration $configuration, $object)
{
if (!preg_match('/^[a-z0-9_]+$/i', $configuration->getName(), $matches)) {
return null;
}
return array($object, $matches[0]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Hateoas\Configuration\Provider\Resolver;
use Hateoas\Configuration\RelationProvider as RelationProviderConfiguration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
interface RelationProviderResolverInterface
{
/**
* @param object $object
* @return callable|null Returns `null` if it does not support this RelationProvider,
* a `callable` otherwise.
*/
public function getRelationProvider(RelationProviderConfiguration $configuration, $object);
}
@@ -0,0 +1,23 @@
<?php
namespace Hateoas\Configuration\Provider\Resolver;
use Hateoas\Configuration\RelationProvider as RelationProviderConfiguration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class StaticMethodResolver implements RelationProviderResolverInterface
{
/**
* {@inheritdoc}
*/
public function getRelationProvider(RelationProviderConfiguration $configuration, $object)
{
if (!preg_match('/^(?P<class>[a-z0-9_\\\\]+)::(?P<method>[a-z0-9_]+)$/i', $configuration->getName(), $matches)) {
return null;
}
return array($matches['class'], $matches['method']);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Hateoas\Configuration\Provider\Resolver;
use Hateoas\Configuration\RelationProvider as RelationProviderConfiguration;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class SymfonyContainerResolver implements RelationProviderResolverInterface
{
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function getRelationProvider(RelationProviderConfiguration $configuration, $object)
{
if (!preg_match('/^(?P<service>[a-z0-9_.]+):(?P<method>[a-z0-9_]+)$/i', $configuration->getName(), $matches)) {
return null;
}
return array(
$this->container->get($matches['service']),
$matches['method']
);
}
}
@@ -0,0 +1,98 @@
<?php
namespace Hateoas\Configuration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Relation
{
/**
* @var string The link "rel" attribute
*/
private $name;
/**
* @var string|Route|null
*/
private $href;
/**
* @var array Extra link attributes
*/
private $attributes;
/**
* @var Embedded|null
*/
private $embedded;
/**
* @var Exclusion|null
*/
private $exclusion;
/**
* @param string $name
* @param string|Route $href
* @param Embedded|string|mixed $embedded
* @param array $attributes
* @param Exclusion $exclusion
*/
public function __construct($name, $href = null, $embedded = null, array $attributes = array(), Exclusion $exclusion = null)
{
if (null !== $embedded && !$embedded instanceof Embedded) {
$embedded = new Embedded($embedded);
}
if (null === !$href && null === $embedded) {
throw new \InvalidArgumentException('$href and $embedded cannot be both null.');
}
$this->name = $name;
$this->href = $href;
$this->embedded = $embedded;
$this->attributes = $attributes;
$this->exclusion = $exclusion;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return Route|string|null
*/
public function getHref()
{
return $this->href;
}
/**
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* @return Embedded|null
*/
public function getEmbedded()
{
return $this->embedded;
}
/**
* @return Exclusion|null
*/
public function getExclusion()
{
return $this->exclusion;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Hateoas\Configuration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class RelationProvider
{
/**
* @var string
*/
private $name;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Hateoas\Configuration;
use Hateoas\Configuration\Provider\RelationProviderInterface as RelationProviderProviderInterface;
use Metadata\MetadataFactoryInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class RelationsRepository
{
/**
* @var MetadataFactoryInterface
*/
private $metadataFactory;
/**
* @var RelationProviderProvider
*/
private $relationProvider;
/**
* @param MetadataFactoryInterface $metadataFactory
* @param RelationProviderProviderInterface $relationProvider
*/
public function __construct(MetadataFactoryInterface $metadataFactory, RelationProviderProviderInterface $relationProvider)
{
$this->metadataFactory = $metadataFactory;
$this->relationProvider = $relationProvider;
}
/**
* @param object $object
*
* @return Relation[]
*/
public function getRelations($object)
{
$relations = array();
if (null !== $classMetadata = $this->metadataFactory->getMetadataForClass(get_class($object))) {
$relations = array_merge($relations, $classMetadata->getRelations());
}
$relations = array_merge($relations, $this->relationProvider->getRelations($object));
return $relations;
}
}
@@ -0,0 +1,75 @@
<?php
namespace Hateoas\Configuration;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Route
{
/**
* @var string
*/
private $name;
/**
* @var string|array
*/
private $parameters;
/**
* @var boolean
*/
private $absolute;
/**
* @var null|string
*/
private $generator;
/**
* @param string $name
* @param string|array $parameters
* @param boolean $absolute
* @param string|null $generator
*/
public function __construct($name, $parameters = array(), $absolute = false, $generator = null)
{
$this->name = $name;
$this->parameters = $parameters;
$this->absolute = $absolute;
$this->generator = $generator;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string|array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* @return boolean
*/
public function isAbsolute()
{
return $this->absolute;
}
/**
* @return null|string
*/
public function getGenerator()
{
return $this->generator;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Hateoas\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
* @author William Durand <william.durand1@gmail.com>
*/
class ExpressionEvaluator
{
const EXPRESSION_REGEX = '/expr\((?P<expression>.+)\)/';
/**
* @var ExpressionLanguage
*/
private $expressionLanguage;
/**
* @var array
*/
private $context;
/**
* @var array
*/
private $cache;
public function __construct(ExpressionLanguage $expressionLanguage, array $context = array(), array $cache = array())
{
$this->expressionLanguage = $expressionLanguage;
$this->context = $context;
$this->cache = $cache;
}
/**
* @param string $name
* @param mixed $value
*/
public function setContextVariable($name, $value)
{
$this->context[$name] = $value;
}
/**
* @param string $expression
* @param mixed $data
* @return mixed
*/
public function evaluate($expression, $data)
{
if (!is_string($expression)) {
return $expression;
}
$key = $expression;
if (!array_key_exists($key, $this->cache)) {
if (!preg_match(self::EXPRESSION_REGEX, $expression, $matches)) {
$this->cache[$key] = false;
} else {
$expression = $matches['expression'];
$context = $this->context;
$context['object'] = $data;
$this->cache[$key] = $this->expressionLanguage->parse($expression, array_keys($context));
}
}
if (false !== $this->cache[$key]) {
if (!isset($context)) {
$context = $this->context;
$context['object'] = $data;
}
return $this->expressionLanguage->evaluate($this->cache[$key], $context);
}
return $expression;
}
public function evaluateArray(array $array, $data)
{
$newArray = array();
foreach ($array as $key => $value) {
$key = $this->evaluate($key, $data);
$value = is_array($value) ? $this->evaluateArray($value, $data) : $this->evaluate($value, $data);
$newArray[$key] = $value;
}
return $newArray;
}
/**
* Register a new new ExpressionLanguage function.
*
* @param ExpressionFunctionInterface $function
*
* @return ExpressionEvaluator
*/
public function registerFunction(ExpressionFunctionInterface $function)
{
$this->expressionLanguage->register(
$function->getName(),
$function->getCompiler(),
$function->getEvaluator()
);
foreach ($function->getContextVariables() as $name => $value) {
$this->setContextVariable($name, $value);
}
return $this;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Hateoas\Expression;
/**
* @author William Durand <william.durand1@gmail.com>
*/
interface ExpressionFunctionInterface
{
/**
* Return the name of the function in an expression.
*
* @return string
*/
public function getName();
/**
* Return a function executed when compiling an expression using the function.
*
* @return closure
*/
public function getCompiler();
/**
* Return a function executed when the expression is evaluated.
*
* @return closure
*/
public function getEvaluator();
/**
* Return context variables as an array.
*
* @return array
*/
public function getContextVariables();
}
@@ -0,0 +1,60 @@
<?php
namespace Hateoas\Expression;
use Hateoas\Helper\LinkHelper;
/**
* @author William Durand <william.durand1@gmail.com>
*/
class LinkExpressionFunction implements ExpressionFunctionInterface
{
/**
* @var LinkHelper
*/
private $linkHelper;
/**
* @param LinkHelper $linkHelper
*/
public function __construct(LinkHelper $linkHelper)
{
$this->linkHelper = $linkHelper;
}
/**
* {@inheritDoc}
*/
public function getName()
{
return 'link';
}
/**
* {@inheritDoc}
*/
public function getCompiler()
{
return function ($object, $rel, $absolute = false) {
return sprintf('$link_helper->getLinkHref(%s, %s, %s)', $object, $rel, $absolute);
};
}
/**
* {@inheritDoc}
*/
public function getEvaluator()
{
return function ($context, $object, $rel, $absolute = false) {
return $context['link_helper']->getLinkHref($object, $rel, $absolute);
};
}
/**
* {@inheritDoc}
*/
public function getContextVariables()
{
return array('link_helper' => $this->linkHelper);
}
}
@@ -0,0 +1,70 @@
<?php
namespace Hateoas\Factory;
use Hateoas\Configuration\RelationsRepository;
use Hateoas\Expression\ExpressionEvaluator;
use Hateoas\Model\Embedded;
use Hateoas\Serializer\ExclusionManager;
use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
use JMS\Serializer\SerializationContext;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class EmbeddedsFactory
{
/**
* @var RelationsRepository
*/
private $relationsRepository;
/**
* @var ExpressionEvaluator
*/
private $expressionEvaluator;
/**
* @var ExclusionManager
*/
private $exclusionManager;
/**
* @param RelationsRepository $relationsRepository
* @param ExpressionEvaluator $expressionEvaluator
* @param ExclusionManager $exclusionManager
*/
public function __construct(
RelationsRepository $relationsRepository,
ExpressionEvaluator $expressionEvaluator,
ExclusionManager $exclusionManager
) {
$this->relationsRepository = $relationsRepository;
$this->expressionEvaluator = $expressionEvaluator;
$this->exclusionManager = $exclusionManager;
}
/**
* @param object $object
* @param SerializationContext $context
* @return Embedded[]
*/
public function create($object, SerializationContext $context)
{
$embeddeds = array();
foreach ($this->relationsRepository->getRelations($object) as $relation) {
if ($this->exclusionManager->shouldSkipEmbedded($object, $relation, $context)) {
continue;
}
$rel = $this->expressionEvaluator->evaluate($relation->getName(), $object);
$data = $this->expressionEvaluator->evaluate($relation->getEmbedded()->getContent(), $object);
$xmlElementName = $this->expressionEvaluator->evaluate($relation->getEmbedded()->getXmlElementName(), $object);
$propertyMetadata = new RelationPropertyMetadata($relation->getEmbedded()->getExclusion(), $relation);
$embeddeds[] = new Embedded($rel, $data, $propertyMetadata, $xmlElementName);
}
return $embeddeds;
}
}
@@ -0,0 +1,80 @@
<?php
namespace Hateoas\Factory;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\Route;
use Hateoas\Expression\ExpressionEvaluator;
use Hateoas\Model\Link;
use Hateoas\UrlGenerator\UrlGeneratorRegistry;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class LinkFactory
{
/**
* @var ExpressionEvaluator
*/
private $expressionEvaluator;
/**
* @var UrlGeneratorRegistry
*/
private $urlGeneratorRegistry;
/**
* @param ExpressionEvaluator $expressionEvaluator
* @param UrlGeneratorRegistry $urlGeneratorRegistry
*/
public function __construct(ExpressionEvaluator $expressionEvaluator, UrlGeneratorRegistry $urlGeneratorRegistry)
{
$this->expressionEvaluator = $expressionEvaluator;
$this->urlGeneratorRegistry = $urlGeneratorRegistry;
}
/**
* @param object $object
* @param Relation $relation
*
* @return Link
*/
public function createLink($object, Relation $relation)
{
$rel = $this->expressionEvaluator->evaluate($relation->getName(), $object);
$href = $relation->getHref();
if ($href instanceof Route) {
if (!$this->urlGeneratorRegistry->hasGenerators()) {
throw new \RuntimeException('You cannot use a route without an url generator.');
}
$name = $this->expressionEvaluator->evaluate($href->getName(), $object);
$parameters = is_array($href->getParameters())
? $this->expressionEvaluator->evaluateArray($href->getParameters(), $object)
: $this->expressionEvaluator->evaluate($href->getParameters(), $object)
;
$isAbsolute = $this->expressionEvaluator->evaluate($href->isAbsolute(), $object);
if (!is_array($parameters)) {
throw new \RuntimeException(
sprintf(
'The route parameters should be an array, %s given. Maybe you forgot to wrap the expression in expr(...).',
gettype($parameters)
)
);
}
$href = $this->urlGeneratorRegistry
->get($href->getGenerator())
->generate($name, $parameters, $isAbsolute)
;
} else {
$href = $this->expressionEvaluator->evaluate($href, $object);
}
$attributes = $this->expressionEvaluator->evaluateArray($relation->getAttributes(), $object);
return new Link($rel, $href, $attributes);
}
}
@@ -0,0 +1,61 @@
<?php
namespace Hateoas\Factory;
use Hateoas\Configuration\RelationsRepository;
use Hateoas\Model\Link;
use Hateoas\Serializer\ExclusionManager;
use JMS\Serializer\SerializationContext;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class LinksFactory
{
/**
* @var RelationsRepository
*/
private $relationsRepository;
/**
* @var LinkFactory
*/
private $linkFactory;
/**
* @var ExclusionManager
*/
private $exclusionManager;
/**
* @param RelationsRepository $relationsRepository
* @param LinkFactory $linkFactory
* @param ExclusionManager $exclusionManager
*/
public function __construct(RelationsRepository $relationsRepository, LinkFactory $linkFactory, ExclusionManager $exclusionManager)
{
$this->relationsRepository = $relationsRepository;
$this->linkFactory = $linkFactory;
$this->exclusionManager = $exclusionManager;
}
/**
* @param object $object
* @param SerializationContext $context
*
* @return Link[]
*/
public function create($object, SerializationContext $context)
{
$links = array();
foreach ($this->relationsRepository->getRelations($object) as $relation) {
if ($this->exclusionManager->shouldSkipLink($object, $relation, $context)) {
continue;
}
$links[] = $this->linkFactory->createLink($object, $relation);
}
return $links;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Hateoas;
use Hateoas\Helper\LinkHelper;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Hateoas implements SerializerInterface
{
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var LinkHelper
*/
private $linkHelper;
/**
* @param SerializerInterface $serializer
* @param LinkHelper $linkHelper
*/
public function __construct(SerializerInterface $serializer, LinkHelper $linkHelper)
{
$this->serializer = $serializer;
$this->linkHelper = $linkHelper;
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, SerializationContext $context = null)
{
return $this->serializer->serialize($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, DeserializationContext $context = null)
{
return $this->serializer->deserialize($data, $type, $format, $context);
}
/**
* @return SerializerInterface
*/
public function getSerializer()
{
return $this->serializer;
}
/**
* @return LinkHelper
*/
public function getLinkHelper()
{
return $this->linkHelper;
}
}
@@ -0,0 +1,521 @@
<?php
namespace Hateoas;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\FileCacheReader;
use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
use Hateoas\Configuration\Metadata\Driver\AnnotationDriver;
use Hateoas\Configuration\Metadata\Driver\ExtensionDriver;
use Hateoas\Configuration\Metadata\Driver\YamlDriver;
use Hateoas\Configuration\Metadata\Driver\XmlDriver;
use Hateoas\Configuration\Provider\Resolver\MethodResolver;
use Hateoas\Configuration\Provider\Resolver\ChainResolver;
use Hateoas\Configuration\Provider\RelationProvider;
use Hateoas\Configuration\Provider\Resolver\RelationProviderResolverInterface;
use Hateoas\Configuration\Provider\Resolver\StaticMethodResolver;
use Hateoas\Configuration\RelationsRepository;
use Hateoas\Expression\ExpressionEvaluator;
use Hateoas\Expression\ExpressionFunctionInterface;
use Hateoas\Expression\LinkExpressionFunction;
use Hateoas\Factory\EmbeddedsFactory;
use Hateoas\Factory\LinkFactory;
use Hateoas\Factory\LinksFactory;
use Hateoas\Helper\LinkHelper;
use Hateoas\UrlGenerator\UrlGeneratorInterface;
use Hateoas\UrlGenerator\UrlGeneratorRegistry;
use Hateoas\Serializer\EventSubscriber\JsonEventSubscriber;
use Hateoas\Serializer\EventSubscriber\XmlEventSubscriber;
use Hateoas\Serializer\ExclusionManager;
use Hateoas\Serializer\JsonHalSerializer;
use Hateoas\Serializer\JsonSerializerInterface;
use Hateoas\Serializer\JMSSerializerMetadataAwareInterface;
use Hateoas\Serializer\Metadata\InlineDeferrer;
use Hateoas\Serializer\XmlSerializer;
use Hateoas\Serializer\XmlSerializerInterface;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
use JMS\Serializer\SerializerBuilder;
use Metadata\Cache\FileCache;
use Metadata\Driver\DriverChain;
use Metadata\Driver\FileLocator;
use Metadata\MetadataFactory;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*
* Some code (metadata things) from this class has been taken from
* https://github.com/schmittjoh/serializer/blob/a29f1e5083654ba2c126acd94ddb2287069b0b5b/src/JMS/Serializer/SerializerBuilder.php
*/
class HateoasBuilder
{
/**
* @var SerializerBuilder
*/
private $serializerBuilder;
/**
* @var ExpressionLanguage
*/
private $expressionLanguage;
/**
* @var array
*/
private $contextVariables = array();
/**
* ExpressionFunctionInterface[]
*/
private $expressionFunctions = array();
/**
* @var XmlSerializerInterface
*/
private $xmlSerializer;
/**
* @var JsonSerializerInterface
*/
private $jsonSerializer;
/**
* @var UrlGeneratorRegistry
*/
private $urlGeneratorRegistry;
private $configurationExtensions = array();
private $chainResolver;
private $metadataDirs = array();
private $debug = false;
private $cacheDir;
private $annotationReader;
private $includeInterfaceMetadata = false;
/**
* @param SerializerBuilder $serializerBuilder
*
* @return HateoasBuilder
*/
public static function create(SerializerBuilder $serializerBuilder = null)
{
return new static($serializerBuilder);
}
/**
* @return Hateoas
*/
public static function buildHateoas()
{
$builder = static::create();
return $builder->build();
}
public function __construct(SerializerBuilder $serializerBuilder = null)
{
$this->serializerBuilder = $serializerBuilder ?: SerializerBuilder::create();
$this->urlGeneratorRegistry = new UrlGeneratorRegistry();
$this->chainResolver = new ChainResolver(array(
new MethodResolver(),
new StaticMethodResolver(),
));
}
/**
* Build a configured Hateoas instance.
*
* @return Hateoas
*/
public function build()
{
$metadataFactory = $this->buildMetadataFactory();
$relationProvider = new RelationProvider($metadataFactory, $this->chainResolver);
$relationsRepository = new RelationsRepository($metadataFactory, $relationProvider);
$expressionEvaluator = new ExpressionEvaluator($this->getExpressionLanguage(), $this->contextVariables);
$linkFactory = new LinkFactory($expressionEvaluator, $this->urlGeneratorRegistry);
$exclusionManager = new ExclusionManager($expressionEvaluator);
$linksFactory = new LinksFactory($relationsRepository, $linkFactory, $exclusionManager);
$embeddedsFactory = new EmbeddedsFactory($relationsRepository, $expressionEvaluator, $exclusionManager);
$linkHelper = new LinkHelper($linkFactory, $relationsRepository);
// Register Hateoas core functions
$expressionEvaluator->registerFunction(new LinkExpressionFunction($linkHelper));
// Register user functions
foreach ($this->expressionFunctions as $expressionFunction) {
$expressionEvaluator->registerFunction($expressionFunction);
}
if (null === $this->xmlSerializer) {
$this->setDefaultXmlSerializer();
}
if (null === $this->jsonSerializer) {
$this->setDefaultJsonSerializer();
}
$inlineDeferrers = array();
$eventSubscribers = array(
new XmlEventSubscriber($this->xmlSerializer, $linksFactory, $embeddedsFactory),
new JsonEventSubscriber(
$this->jsonSerializer,
$linksFactory,
$embeddedsFactory,
$inlineDeferrers[] = new InlineDeferrer(),
$inlineDeferrers[] = new InlineDeferrer()
),
);
$this->serializerBuilder
->addDefaultListeners()
->configureListeners(function (EventDispatcherInterface $dispatcher) use ($eventSubscribers) {
foreach ($eventSubscribers as $eventSubscriber) {
$dispatcher->addSubscriber($eventSubscriber);
}
})
;
$jmsSerializer = $this->serializerBuilder->build();
foreach (array_merge($inlineDeferrers, array($this->jsonSerializer, $this->xmlSerializer)) as $serializer) {
if ($serializer instanceof JMSSerializerMetadataAwareInterface) {
$serializer->setMetadataFactory($jmsSerializer->getMetadataFactory());
}
}
return new Hateoas($jmsSerializer, $linkHelper);
}
/**
* @param XmlSerializerInterface $xmlSerializer
*
* @return HateoasBuilder
*/
public function setXmlSerializer(XmlSerializerInterface $xmlSerializer)
{
$this->xmlSerializer = $xmlSerializer;
return $this;
}
/**
* Set the default XML serializer (`XmlSerializer`).
*
* @return HateoasBuilder
*/
public function setDefaultXmlSerializer()
{
return $this->setXmlSerializer(new XmlSerializer());
}
/**
* @param JsonSerializerInterface $jsonSerializer
*
* @return HateoasBuilder
*/
public function setJsonSerializer(JsonSerializerInterface $jsonSerializer)
{
$this->jsonSerializer = $jsonSerializer;
return $this;
}
/**
* Set the default JSON serializer (`JsonHalSerializer`).
*
* @return HateoasBuilder
*/
public function setDefaultJsonSerializer()
{
return $this->setJsonSerializer(new JsonHalSerializer());
}
/**
* Add a new URL generator. If you pass `null` as name, it will be the
* default URL generator.
*
* @param string|null $name
* @param UrlGeneratorInterface $urlGenerator
*
* @return HateoasBuilder
*/
public function setUrlGenerator($name, UrlGeneratorInterface $urlGenerator)
{
$this->urlGeneratorRegistry->set($name, $urlGenerator);
return $this;
}
/**
* Add a new expression context variable.
*
* @param string $name
* @param mixed $value
*
* @return HateoasBuilder
*/
public function setExpressionContextVariable($name, $value)
{
$this->contextVariables[$name] = $value;
return $this;
}
/**
* @param ExpressionLanguage $expressionLanguage
*
* @return HateoasBuilder
*/
public function setExpressionLanguage(ExpressionLanguage $expressionLanguage)
{
$this->expressionLanguage = $expressionLanguage;
return $this;
}
/**
* @param ExpressionFunctionInterface $expressionFunction
*
* @return HateoasBuilder
*/
public function registerExpressionFunction(ExpressionFunctionInterface $expressionFunction)
{
$this->expressionFunctions[] = $expressionFunction;
return $this;
}
/**
* Add a new relation provider resolver.
*
* @param RelationProviderResolverInterface $resolver
*
* @return HateoasBuilder
*/
public function addRelationProviderResolver(RelationProviderResolverInterface $resolver)
{
$this->chainResolver->addResolver($resolver);
return $this;
}
/**
* @param ConfigurationExtensionInterface $configurationExtension
*
* @return HateoasBuilder
*/
public function addConfigurationExtension(ConfigurationExtensionInterface $configurationExtension)
{
$this->configurationExtensions[] = $configurationExtension;
return $this;
}
/**
* @param boolean $debug
*
* @return HateoasBuilder
*/
public function setDebug($debug)
{
$this->debug = (boolean) $debug;
return $this;
}
/**
* @param string $dir
*
* @return HateoasBuilder
*/
public function setCacheDir($dir)
{
if (!is_dir($dir)) {
$this->createDir($dir);
}
if (!is_writable($dir)) {
throw new \InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
}
$this->cacheDir = $dir;
return $this;
}
/**
* @param boolean $include Whether to include the metadata from the interfaces
*
* @return HateoasBuilder
*/
public function includeInterfaceMetadata($include)
{
$this->includeInterfaceMetadata = (boolean) $include;
return $this;
}
/**
* Set a map of namespace prefixes to directories.
*
* This method overrides any previously defined directories.
*
* @param array $namespacePrefixToDirMap
*
* @return HateoasBuilder
*/
public function setMetadataDirs(array $namespacePrefixToDirMap)
{
foreach ($namespacePrefixToDirMap as $dir) {
if (!is_dir($dir)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
}
}
$this->metadataDirs = $namespacePrefixToDirMap;
return $this;
}
/**
* Add a directory where the serializer will look for class metadata.
*
* The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
* that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
*
* If you use an empty prefix, your metadata files would need to look like:
*
* ``my-dir/MyApplication.Entity.SomeObject.yml``
* ``my-dir/MyApplication.Entity.OtherObject.yml``
*
* If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
*
* ``my-dir/SomeObject.yml``
* ``my-dir/OtherObject.yml``
*
* Please keep in mind that you currently may only have one directory per namespace prefix.
*
* @param string $dir The directory where metadata files are located.
* @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
*
* @return HateoasBuilder
*/
public function addMetadataDir($dir, $namespacePrefix = '')
{
if (!is_dir($dir)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
}
if (isset($this->metadataDirs[$namespacePrefix])) {
throw new \InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix));
}
$this->metadataDirs[$namespacePrefix] = $dir;
return $this;
}
/**
* Add a map of namespace prefixes to directories.
*
* @param array $namespacePrefixToDirMap
*
* @return HateoasBuilder
*/
public function addMetadataDirs(array $namespacePrefixToDirMap)
{
foreach ($namespacePrefixToDirMap as $prefix => $dir) {
$this->addMetadataDir($dir, $prefix);
}
return $this;
}
/**
* Similar to addMetadataDir(), but overrides an existing entry.
*
* @param string $dir
* @param string $namespacePrefix
*
* @return HateoasBuilder
*/
public function replaceMetadataDir($dir, $namespacePrefix = '')
{
if (!is_dir($dir)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
}
if (!isset($this->metadataDirs[$namespacePrefix])) {
throw new \InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix));
}
$this->metadataDirs[$namespacePrefix] = $dir;
return $this;
}
private function buildMetadataFactory()
{
$annotationReader = $this->annotationReader;
if (null === $annotationReader) {
$annotationReader = new AnnotationReader();
if (null !== $this->cacheDir) {
$this->createDir($this->cacheDir.'/annotations');
$annotationReader = new FileCacheReader($annotationReader, $this->cacheDir.'/annotations', $this->debug);
}
}
if (!empty($this->metadataDirs)) {
$fileLocator = new FileLocator($this->metadataDirs);
$metadataDriver = new DriverChain(array(
new YamlDriver($fileLocator),
new XmlDriver($fileLocator),
new AnnotationDriver($annotationReader),
));
} else {
$metadataDriver = new AnnotationDriver($annotationReader);
}
$metadataDriver = new ExtensionDriver($metadataDriver, $this->configurationExtensions);
$metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);
$metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);
if (null !== $this->cacheDir) {
$this->createDir($this->cacheDir.'/metadata');
$metadataFactory->setCache(new FileCache($this->cacheDir.'/metadata'));
}
return $metadataFactory;
}
/**
* @param string $dir
*/
private function createDir($dir)
{
if (is_dir($dir)) {
return;
}
if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new \RuntimeException(sprintf('Could not create directory "%s".', $dir));
}
}
private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
$this->expressionLanguage = new ExpressionLanguage();
}
return $this->expressionLanguage;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Hateoas\Helper;
use Hateoas\Configuration\RelationsRepository;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\Route;
use Hateoas\Factory\LinkFactory;
/**
* @author William Durand <william.durand1@gmail.com>
*/
class LinkHelper
{
/**
* @var LinkFactory
*/
private $linkFactory;
/**
* @var RelationsRepository
*/
private $relationsRepository;
/**
* @param LinkFactory $linkFactory
* @param RelationsRepository $relationsRepository
*/
public function __construct(LinkFactory $linkFactory, RelationsRepository $relationsRepository)
{
$this->linkFactory = $linkFactory;
$this->relationsRepository = $relationsRepository;
}
/**
* @param object $object
* @param string $rel
* @param boolean $absolute
*
* @return string
*/
public function getLinkHref($object, $rel, $absolute = false)
{
foreach ($this->relationsRepository->getRelations($object) as $relation) {
if ($rel === $relation->getName()) {
$relation = $this->patchAbsolute($relation, $absolute);
if (null !== $link = $this->linkFactory->createLink($object, $relation)) {
return $link->getHref();
}
}
}
return null;
}
private function patchAbsolute(Relation $relation, $absolute)
{
$href = $relation->getHref();
if ($href instanceof Route) {
$href = new Route(
$href->getName(),
$href->getParameters(),
$absolute,
$href->getGenerator()
);
}
return new Relation(
$relation->getName(),
$href,
$relation->getEmbedded(),
$relation->getAttributes(),
$relation->getExclusion()
);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Hateoas\Model;
use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Embedded
{
/**
* @var string
*/
private $rel;
/**
* @var mixed
*/
private $data;
/**
* @var string|null
*/
private $xmlElementName;
/**
* @var RelationPropertyMetadata
*/
private $metadata;
/**
* @param string $rel
* @param mixed $data
* @param string|null $xmlElementName
* @param RelationPropertyMetadata $metadata
*/
public function __construct($rel, $data, RelationPropertyMetadata $metadata, $xmlElementName = null)
{
$this->rel = $rel;
$this->data = $data;
$this->metadata = $metadata;
$this->xmlElementName = $xmlElementName;
}
/**
* @return RelationPropertyMetadata
*/
public function getMetadata()
{
return $this->metadata;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @return string
*/
public function getRel()
{
return $this->rel;
}
/**
* @return null|string
*/
public function getXmlElementName()
{
return $this->xmlElementName;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Hateoas\Model;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class Link
{
/**
* @var string
*/
private $rel;
/**
* @var string
*/
private $href;
/**
* @var array
*/
private $attributes;
/**
* @param string $rel
* @param string $href
* @param array $attributes
*/
public function __construct($rel, $href, array $attributes = array())
{
$this->rel = $rel;
$this->href = $href;
$this->attributes = $attributes;
}
/**
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* @return string
*/
public function getHref()
{
return $this->href;
}
/**
* @return string
*/
public function getRel()
{
return $this->rel;
}
}
@@ -0,0 +1,104 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
*
* @author Premi Giorgio <giosh94mhz@gmail.com>
*/
abstract class AbstractSegmentedRepresentation extends RouteAwareRepresentation
{
/**
* @var int
*
* @Serializer\Expose
* @Serializer\Type("integer")
* @Serializer\XmlAttribute
*/
private $limit;
/**
* @var int
*
* @Serializer\Expose
* @Serializer\Type("integer")
* @Serializer\XmlAttribute
*/
private $total;
/**
* @var string
*/
private $limitParameterName;
public function __construct(
$inline,
$route,
array $parameters = array(),
$limit,
$total = null,
$limitParameterName = null,
$absolute = false
) {
parent::__construct($inline, $route, $parameters, $absolute);
$this->total = $total;
$this->limit = $limit;
$this->limitParameterName = $limitParameterName ?: 'limit';
}
/**
* @return int
*/
public function getLimit()
{
return $this->limit;
}
/**
* @param null $limit
* @return array
*/
public function getParameters($limit = null)
{
$parameters = parent::getParameters();
$parameters[$this->limitParameterName] = null === $limit ? $this->getLimit() : $limit;
return $parameters;
}
/**
* @return int
*/
public function getTotal()
{
return $this->total;
}
/**
* @return string
*/
public function getLimitParameterName()
{
return $this->limitParameterName;
}
/**
* @param string $key
*/
protected function moveParameterToEnd(array &$parameters, $key)
{
if (! array_key_exists($key, $parameters)) {
return;
}
$value = $parameters[$key];
unset($parameters[$key]);
$parameters[$key] = $value;
}
}
@@ -0,0 +1,132 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use Hateoas\Configuration\Embedded;
use Hateoas\Configuration\Exclusion;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use Hateoas\Configuration\Relation;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
* @Serializer\XmlRoot("collection")
*
* @Hateoas\RelationProvider("getRelations")
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class CollectionRepresentation
{
/**
* @var string
*/
private $rel;
/**
* @var mixed
*/
private $resources;
/**
* @var null|string
*/
private $xmlElementName;
/**
* @var null|Exclusion
*/
private $exclusion;
/**
* @var null|Exclusion
*/
private $embedExclusion;
/**
* @var array
*/
private $relations;
/**
* @param string $rel
* @param string $xmlElementName
*/
public function __construct(
$resources,
$rel = null,
$xmlElementName = null,
Exclusion $exclusion = null,
Exclusion $embedExclusion = null,
array $relations = array()
) {
if ($resources instanceof \Traversable) {
$resources = iterator_to_array($resources);
}
$this->resources = $resources;
$this->rel = $rel ?: 'items';
$this->xmlElementName = $xmlElementName;
$this->exclusion = $exclusion;
$this->embedExclusion = $embedExclusion;
$this->relations = $relations;
}
/**
* @return string
*/
public function getRel()
{
return $this->rel;
}
/**
* @param string $rel
*/
public function setRel($rel)
{
$this->rel = $rel;
}
/**
* @return mixed
*/
public function getResources()
{
return $this->resources;
}
/**
* @return null|string
*/
public function getXmlElementName()
{
return $this->xmlElementName;
}
/**
* @param null|string $xmlElementName
*/
public function setXmlElementName($xmlElementName)
{
$this->xmlElementName = $xmlElementName;
}
public function getRelations($object, ClassMetadataInterface $classMetadata)
{
return array_merge(array(
new Relation(
$this->rel,
null,
new Embedded(
'expr(object.getResources())',
$this->xmlElementName,
$this->embedExclusion
),
array(),
$this->exclusion
)
), $this->relations);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Hateoas\Representation\Factory;
use Hateoas\Configuration\Route;
use Hateoas\Representation\CollectionRepresentation;
use Hateoas\Representation\PaginatedRepresentation;
use Pagerfanta\Pagerfanta;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class PagerfantaFactory
{
/**
* @var string
*/
private $pageParameterName;
/**
* @var string
*/
private $limitParameterName;
/**
* @param string $pageParameterName
* @param string $limitParameterName
*/
public function __construct($pageParameterName = null, $limitParameterName = null)
{
$this->pageParameterName = $pageParameterName;
$this->limitParameterName = $limitParameterName;
}
/**
* @param Pagerfanta $pager The pager
* @param Route $route The collection's route
* @param mixed $inline Most of the time, a custom `CollectionRepresentation` instance
*
* @return PaginatedRepresentation
*/
public function createRepresentation(Pagerfanta $pager, Route $route, $inline = null)
{
if (null === $inline) {
$inline = new CollectionRepresentation($pager->getCurrentPageResults());
}
return new PaginatedRepresentation(
$inline,
$route->getName(),
$route->getParameters(),
$pager->getCurrentPage(),
$pager->getMaxPerPage(),
$pager->getNbPages(),
$this->getPageParameterName(),
$this->getLimitParameterName(),
$route->isAbsolute(),
$pager->getNbResults()
);
}
/**
* @return string
*/
public function getPageParameterName()
{
return $this->pageParameterName;
}
/**
* @return string
*/
public function getLimitParameterName()
{
return $this->limitParameterName;
}
}
@@ -0,0 +1,134 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
* @Serializer\XmlRoot("collection")
* @Serializer\AccessorOrder("custom", custom = {"offset", "limit", "total"})
*
* @Hateoas\Relation(
* "first",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(0))",
* absolute = "expr(object.isAbsolute())"
* )
* )
* @Hateoas\Relation(
* "last",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters((object.getTotal() - 1) - (object.getTotal() - 1) % object.getLimit()))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(object.getTotal() === null)"
* )
* )
* @Hateoas\Relation(
* "next",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(object.getOffset() + object.getLimit()))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(object.getTotal() !== null && (object.getOffset() + object.getLimit()) >= object.getTotal())"
* )
* )
* @Hateoas\Relation(
* "previous",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters((object.getOffset() > object.getLimit()) ? object.getOffset() - object.getLimit() : 0))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(! object.getOffset())"
* )
* )
*
* @author Premi Giorgio <giosh94mhz@gmail.com>
*/
class OffsetRepresentation extends AbstractSegmentedRepresentation
{
/**
* @var int
*
* @Serializer\Expose
* @Serializer\XmlAttribute
*/
private $offset;
/**
* @var string
*/
private $offsetParameterName;
/**
* @param CollectionRepresentation $inline
* @param string $route
* @param integer|null $offset
* @param integer $limit
* @param integer $total
*/
public function __construct(
$inline,
$route,
array $parameters = array(),
$offset,
$limit,
$total = null,
$offsetParameterName = null,
$limitParameterName = null,
$absolute = false
) {
parent::__construct($inline, $route, $parameters, $limit, $total, $limitParameterName, $absolute);
$this->offset = $offset;
$this->offsetParameterName = $offsetParameterName ?: 'offset';
}
/**
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* @param null $offset
* @param null $limit
* @return array
*/
public function getParameters($offset = null, $limit = null)
{
$parameters = parent::getParameters($limit);
unset($parameters[$this->offsetParameterName]);
if (null === $offset) {
$offset = $this->getOffset();
}
if ($offset) {
$parameters[$this->offsetParameterName] = $offset;
$this->moveParameterToEnd($parameters, $this->getLimitParameterName());
}
return $parameters;
}
/**
* @return string
*/
public function getOffsetParameterName()
{
return $this->offsetParameterName;
}
}
@@ -0,0 +1,141 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
* @Serializer\XmlRoot("collection")
* @Serializer\AccessorOrder("custom", custom = {"page", "limit", "pages", "total"})
*
* @Hateoas\Relation(
* "first",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(1))",
* absolute = "expr(object.isAbsolute())"
* )
* )
* @Hateoas\Relation(
* "last",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(object.getPages()))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(object.getPages() === null)"
* )
* )
* @Hateoas\Relation(
* "next",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(object.getPage() + 1))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr(object.getPages() !== null && (object.getPage() + 1) > object.getPages())"
* )
* )
* @Hateoas\Relation(
* "previous",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters(object.getPage() - 1))",
* absolute = "expr(object.isAbsolute())"
* ),
* exclusion = @Hateoas\Exclusion(
* excludeIf = "expr((object.getPage() - 1) < 1)"
* )
* )
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class PaginatedRepresentation extends AbstractSegmentedRepresentation
{
/**
* @var int
*
* @Serializer\Expose
* @Serializer\Type("integer")
* @Serializer\XmlAttribute
*/
private $page;
/**
* @var int
*
* @Serializer\Expose
* @Serializer\Type("integer")
* @Serializer\XmlAttribute
*/
private $pages;
/**
* @var string
*/
private $pageParameterName;
public function __construct(
$inline,
$route,
array $parameters = array(),
$page,
$limit,
$pages,
$pageParameterName = null,
$limitParameterName = null,
$absolute = false,
$total = null
) {
parent::__construct($inline, $route, $parameters, $limit, $total, $limitParameterName, $absolute);
$this->page = $page;
$this->pages = $pages;
$this->pageParameterName = $pageParameterName ?: 'page';
}
/**
* @return int
*/
public function getPage()
{
return $this->page;
}
/**
* @param null $page
* @param null $limit
* @return array
*/
public function getParameters($page = null, $limit = null)
{
$parameters = parent::getParameters($limit);
unset($parameters[$this->pageParameterName]);
$parameters[$this->pageParameterName] = null === $page ? $this->getPage() : $page;
$this->moveParameterToEnd($parameters, $this->getLimitParameterName());
return $parameters;
}
/**
* @return int
*/
public function getPages()
{
return $this->pages;
}
/**
* @return string
*/
public function getPageParameterName()
{
return $this->pageParameterName;
}
}
@@ -0,0 +1,84 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Annotation as Hateoas;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
*
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "expr(object.getRoute())",
* parameters = "expr(object.getParameters())",
* absolute = "expr(object.isAbsolute())"
* )
* )
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class RouteAwareRepresentation
{
/**
* @Serializer\Inline
* @Serializer\Expose
*/
private $inline;
/**
* @var string
*/
private $route;
/**
* @var array
*/
private $parameters;
/**
* @var boolean
*/
private $absolute;
public function __construct($inline, $route, array $parameters = array(), $absolute = false)
{
$this->inline = $inline;
$this->route = $route;
$this->parameters = $parameters;
$this->absolute = $absolute;
}
/**
* @return mixed
*/
public function getInline()
{
return $this->inline;
}
/**
* @return string
*/
public function getRoute()
{
return $this->route;
}
/**
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* @return boolean
*/
public function isAbsolute()
{
return $this->absolute;
}
}
@@ -0,0 +1,77 @@
<?php
namespace Hateoas\Representation;
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\Annotation as Hateoas;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\ExclusionPolicy("all")
* @Serializer\XmlRoot("resource")
*
* @Hateoas\RelationProvider("getRelations")
*
* @author William Durand <william.durand1@gmail.com>
*/
class VndErrorRepresentation
{
/**
* @Serializer\Expose
*/
private $message;
/**
* @Serializer\Expose
* @Serializer\XmlAttribute
*/
private $logref;
/**
* @var Relation
*/
private $help;
/**
* @var Relation
*/
private $describes;
/**
* @param string $message
* @param integer $logref
*/
public function __construct($message, $logref = null, Relation $help = null, Relation $describes = null)
{
$this->message = $message;
$this->logref = $logref;
$this->help = $help;
$this->describes = $describes;
}
public function getRelations($object, ClassMetadataInterface $classMetadata)
{
$relations = array();
if (null !== $this->help) {
$relations[] = $this->help;
}
if (null !== $this->describes) {
$relations[] = $this->describes;
}
return $relations;
}
public function getMessage()
{
return $this->message;
}
public function getLogref()
{
return $this->logref;
}
}

Some files were not shown because too many files have changed in this diff Show More