├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── lib ├── AbstractLink.php ├── Curie.php ├── Deserialization │ ├── Builder.php │ ├── Construction │ │ └── ProxyObjectConstruction.php │ ├── Handler │ │ ├── ArrayCollectionHandler.php │ │ └── DateHandler.php │ └── ResourceDeserializationVisitor.php ├── EntryPoint.php ├── Exception │ └── RequestException.php ├── HttpClient │ ├── FileGetContentsHttpClient.php │ ├── HttpClientInterface.php │ └── HttpResponse.php ├── Link.php ├── Proxy │ ├── HalResourceEntity.php │ └── HalResourceEntityInterface.php ├── Resource.php └── ResourceCollection.php ├── phpunit.xml.dist └── tests ├── HalClient ├── Deserialization │ ├── Construction │ │ └── ProxyObjectConstructionTest.php │ └── DeserializationTest.php ├── EntryPointTest.php ├── HttpClient │ └── FileGetContentsHttpClientTest.php ├── LinkTest.php └── ResourceTest.php ├── autoload.php.dist ├── bootstrap.php └── fixtures ├── documents.json ├── entry_point.json └── entry_point_invalid_self.json /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | /nbproject/ 3 | /coverage/ 4 | /vendor/ 5 | /docs/_build 6 | composer.lock 7 | composer.phar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - hhvm 7 | 8 | before_script: 9 | - composer install --dev --prefer-source 10 | 11 | script: make test 12 | 13 | matrix: 14 | allow_failures: 15 | - php: hhvm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ekino 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | phpunit 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HalClient 2 | ========= 3 | 4 | [![Build Status](https://secure.travis-ci.org/ekino/php-hal-client.png)](https://secure.travis-ci.org/#!/ekino/php-hal-client) 5 | 6 | HalClient is a lightweight library to consume HAL resources. 7 | 8 | ### Installation using Composer 9 | 10 | Add the dependency: 11 | 12 | ```bash 13 | php composer.phar require ekino/hal-client 14 | ``` 15 | 16 | If asked for a version, type in 'dev-master' (unless you want another version): 17 | 18 | ```bash 19 | Please provide a version constraint for the ekino/hal-client requirement: dev-master 20 | ``` 21 | 22 | ### Limitations 23 | 24 | There is no support for POST/PUT/PATCH/DELETE methods. 25 | 26 | ### Usage 27 | 28 | ```php 29 | 30 | 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' 35 | )); 36 | 37 | // create an entry point to retrieve the data 38 | $entryPoint = new EntryPoint('/', $client); 39 | $resource = $entryPoint->get(); // return the main resource 40 | 41 | // retrieve a Resource object, which acts as a Pager 42 | $pager = $resource->get('p:documents'); 43 | 44 | $pager->get('page'); 45 | 46 | $collection = $pager->get('documents'); // return a ResourceCollection 47 | 48 | // a ResourceCollection implements the \Iterator and \Countable interface 49 | foreach ($collection as $document) { 50 | // the document is a resource object 51 | $document->get('title'); 52 | } 53 | 54 | ``` 55 | 56 | 57 | ### Integrate JMS/Deserializer 58 | 59 | The library support deserialization of Resource object into native PHP object. 60 | 61 | ```php 62 | 63 | $serializer = Ekino\HalClient\Deserialization\Builder::build(); 64 | 65 | $object = $serializer->deserialize($resource, 'Acme\Article', 'hal'); 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ekino/hal-client", 3 | "description": "HalClient", 4 | "keywords": ["hal", "Hypermedia"], 5 | "homepage": "https://github.com/ekino/php-hal-client", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Thomas Rabaix", 11 | "email": "thomas.rabaix@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.4.0", 16 | "guzzle/parser": "~3.9", 17 | "doctrine/collections": "~1.2" 18 | }, 19 | "require-dev": { 20 | "jms/serializer": "~0.16", 21 | "doctrine/collections": "~1.2" 22 | }, 23 | "suggest": { 24 | "jms/serializer": "Enable support of Resource Deserialization" 25 | }, 26 | "autoload": { 27 | "psr-4": { "Ekino\\HalClient\\": "lib" } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/AbstractLink.php: -------------------------------------------------------------------------------- 1 | name = isset($data['name']) ? $data['name'] : null; 43 | $this->href = isset($data['href']) ? $data['href'] : null; 44 | $this->templated = isset($data['templated']) ? (boolean) $data['templated'] : false; 45 | } 46 | 47 | /** 48 | * @return null|string 49 | */ 50 | public function getName() 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * Returns the href. 57 | * 58 | * @param array $variables Required if the link is templated 59 | * 60 | * @return null|string 61 | * 62 | * @throws \RuntimeException When call with property "href" empty and sets variables 63 | */ 64 | public function getHref(array $variables = array()) 65 | { 66 | if (!empty($variables)) { 67 | return $this->prepareUrl($variables); 68 | } 69 | 70 | return $this->href; 71 | } 72 | 73 | /** 74 | * @return bool 75 | */ 76 | public function isTemplated() 77 | { 78 | return $this->templated; 79 | } 80 | 81 | /** 82 | * Prepare the url with variables. 83 | * 84 | * @param array $variables Required if the link is templated 85 | * 86 | * @return string 87 | * 88 | * @throws \RuntimeException When call with property "href" empty 89 | */ 90 | private function prepareUrl(array $variables = array()) 91 | { 92 | if (null === $this->href) { 93 | throw new \RuntimeException('Href must to be sets.'); 94 | } 95 | 96 | if (!$this->templated) { 97 | return $this->href; 98 | } 99 | 100 | $template = new UriTemplate(); 101 | 102 | return $template->expand($this->href, $variables); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/Curie.php: -------------------------------------------------------------------------------- 1 | setDeserializationVisitor('hal', new ResourceDeserializationVisitor(new CamelCaseNamingStrategy(), $autoload)); 35 | $serializerBuilder->configureHandlers(function($handlerRegistry) { 36 | $handlerRegistry->registerSubscribingHandler(new DateHandler()); 37 | $handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler()); 38 | }); 39 | 40 | if ($cacheDir) { 41 | $serializerBuilder->setCacheDir($cacheDir); 42 | } 43 | 44 | return $serializerBuilder; 45 | } 46 | 47 | /** 48 | * @param bool $autoload 49 | * @param bool $cacheDir 50 | * 51 | * @return Serializer 52 | */ 53 | static function build($autoload = true, $cacheDir = false) 54 | { 55 | return self::get($autoload, $cacheDir)->build(); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/Deserialization/Construction/ProxyObjectConstruction.php: -------------------------------------------------------------------------------- 1 | patterns = $patterns; 38 | } 39 | 40 | /** 41 | * @param Serializer $serializer 42 | */ 43 | public function setSerializer(Serializer $serializer) 44 | { 45 | $this->serializer = $serializer; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context) 52 | { 53 | $pos = strrpos($metadata->name, '\\'); 54 | 55 | $instance = false; 56 | foreach ($this->patterns as $pattern) { 57 | $name = str_replace(['{ns}', '{ln}'], [substr($metadata->name, 0, $pos), substr($metadata->name, $pos + 1)], $pattern); 58 | 59 | if (class_exists($name, true)) { 60 | $instance = unserialize(sprintf('O:%d:"%s":0:{}', strlen($name), $name)); 61 | 62 | break; 63 | } 64 | } 65 | 66 | if (!$instance) { 67 | $instance = unserialize(sprintf('O:%d:"%s":0:{}', strlen($metadata->name), $metadata->name)); 68 | } 69 | 70 | if ($instance instanceof HalResourceEntityInterface && $data instanceof Resource) { 71 | $instance->setHalResource($data); 72 | if ($this->serializer) { 73 | $instance->setHalSerializer($this->serializer); 74 | } 75 | } 76 | 77 | return $instance; 78 | } 79 | } -------------------------------------------------------------------------------- /lib/Deserialization/Handler/ArrayCollectionHandler.php: -------------------------------------------------------------------------------- 1 | GraphNavigator::DIRECTION_DESERIALIZATION, 35 | 'type' => $type, 36 | 'format' => $format, 37 | 'method' => 'deserializeCollection', 38 | ); 39 | } 40 | } 41 | 42 | return $methods; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/Deserialization/Handler/DateHandler.php: -------------------------------------------------------------------------------- 1 | 'DateTime', 33 | 'direction' => GraphNavigator::DIRECTION_DESERIALIZATION, 34 | 'format' => $format, 35 | ); 36 | } 37 | 38 | return $methods; 39 | } 40 | 41 | public function __construct($defaultFormat = \DateTime::ISO8601, $defaultTimezone = 'UTC') 42 | { 43 | $this->defaultFormat = $defaultFormat; 44 | $this->defaultTimezone = new \DateTimeZone($defaultTimezone); 45 | } 46 | 47 | private function parseDateTime($data, array $type) 48 | { 49 | $timezone = isset($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone; 50 | $format = $this->getFormat($type); 51 | $datetime = \DateTime::createFromFormat($format, (string) $data, $timezone); 52 | if (false === $datetime) { 53 | throw new RuntimeException(sprintf('Invalid datetime "%s", expected format %s.', $data, $format)); 54 | } 55 | 56 | return $datetime; 57 | } 58 | 59 | /** 60 | * @return string 61 | * @param array $type 62 | */ 63 | private function getFormat(array $type) 64 | { 65 | return isset($type['params'][0]) ? $type['params'][0] : $this->defaultFormat; 66 | } 67 | 68 | public function deserializeDateTimeFromhal(ResourceDeserializationVisitor $visitor, $data, array $type) 69 | { 70 | if (null === $data) { 71 | return null; 72 | } 73 | 74 | return $this->parseDateTime($data, $type); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/Deserialization/ResourceDeserializationVisitor.php: -------------------------------------------------------------------------------- 1 | namingStrategy = $namingStrategy; 42 | $this->autoload = $autoload; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function decode($str) 49 | { 50 | if (!$str instanceof Resource) { 51 | throw new \RuntimeException('Invalid argument, Ekino\HalClient\Resource required'); 52 | } 53 | 54 | return $str; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function visitProperty(PropertyMetadata $metadata, $data, Context $context) 61 | { 62 | $name = $this->namingStrategy->translateName($metadata); 63 | 64 | if ($data instanceof Resource && !$data->hasEmbedded($name) && $data->hasLink($name) && !$data->hasProperty($name) && !$this->autoload) { 65 | return; 66 | } 67 | 68 | if (isset($data[$name]) === false) { 69 | return; 70 | } 71 | 72 | if (!$metadata->type) { 73 | throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name)); 74 | } 75 | 76 | $v = $data[$name] !== null ? $this->getNavigator()->accept($data[$name], $metadata->type, $context) : null; 77 | 78 | if (null === $metadata->setter) { 79 | $metadata->reflection->setValue($this->getCurrentObject(), $v); 80 | 81 | return; 82 | } 83 | 84 | $this->getCurrentObject()->{$metadata->setter}($v); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function setNavigator(GraphNavigator $navigator) 91 | { 92 | $this->navigator = $navigator; 93 | $this->result = null; 94 | $this->objectStack = new \SplStack; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function getNavigator() 101 | { 102 | return $this->navigator; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function prepare($data) 109 | { 110 | return $this->decode($data); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function visitNull($data, array $type, Context $context) 117 | { 118 | return null; 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function visitString($data, array $type, Context $context) 125 | { 126 | $data = (string) $data; 127 | 128 | if (null === $this->result) { 129 | $this->result = $data; 130 | } 131 | 132 | return $data; 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function visitBoolean($data, array $type, Context $context) 139 | { 140 | $data = (Boolean) $data; 141 | 142 | if (null === $this->result) { 143 | $this->result = $data; 144 | } 145 | 146 | return $data; 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | public function visitInteger($data, array $type, Context $context) 153 | { 154 | $data = (integer) $data; 155 | 156 | if (null === $this->result) { 157 | $this->result = $data; 158 | } 159 | 160 | return $data; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function visitDouble($data, array $type, Context $context) 167 | { 168 | $data = (double) $data; 169 | 170 | if (null === $this->result) { 171 | $this->result = $data; 172 | } 173 | 174 | return $data; 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function visitArray($data, array $type, Context $context) 181 | { 182 | if ( ! $data instanceof ResourceCollection && !is_array($data) ) { 183 | throw new RuntimeException(sprintf('Expected ResourceCollection or array, but got %s: %s', gettype($data), json_encode($data))); 184 | } 185 | 186 | // If no further parameters were given, keys/values are just passed as is. 187 | if ( ! $type['params']) { 188 | if (null === $this->result) { 189 | $this->result = $data; 190 | } 191 | 192 | return $data; 193 | } 194 | 195 | switch (count($type['params'])) { 196 | case 1: // Array is a list. 197 | $listType = $type['params'][0]; 198 | 199 | $result = array(); 200 | if (null === $this->result) { 201 | $this->result = &$result; 202 | } 203 | 204 | foreach ($data as $v) { 205 | $result[] = $this->navigator->accept($v, $listType, $context); 206 | } 207 | 208 | return $result; 209 | 210 | case 2: // Array is a map. 211 | list($keyType, $entryType) = $type['params']; 212 | 213 | $result = array(); 214 | if (null === $this->result) { 215 | $this->result = &$result; 216 | } 217 | 218 | foreach ($data as $k => $v) { 219 | $result[$this->navigator->accept($k, $keyType, $context)] = $this->navigator->accept($v, $entryType, $context); 220 | } 221 | 222 | return $result; 223 | 224 | default: 225 | throw new RuntimeException(sprintf('Array type cannot have more than 2 parameters, but got %s.', json_encode($type['params']))); 226 | } 227 | } 228 | 229 | /** 230 | * {@inheritdoc} 231 | */ 232 | public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context) 233 | { 234 | $this->setCurrentObject($object); 235 | 236 | if (null === $this->result) { 237 | $this->result = $this->currentObject; 238 | } 239 | } 240 | 241 | /** 242 | * {@inheritdoc} 243 | */ 244 | public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context) 245 | { 246 | $obj = $this->currentObject; 247 | $this->revertCurrentObject(); 248 | 249 | return $obj; 250 | } 251 | 252 | /** 253 | * {@inheritdoc} 254 | */ 255 | public function getResult() 256 | { 257 | return $this->result; 258 | } 259 | 260 | public function setCurrentObject($object) 261 | { 262 | $this->objectStack->push($this->currentObject); 263 | $this->currentObject = $object; 264 | } 265 | 266 | public function getCurrentObject() 267 | { 268 | return $this->currentObject; 269 | } 270 | 271 | public function revertCurrentObject() 272 | { 273 | return $this->currentObject = $this->objectStack->pop(); 274 | } 275 | } -------------------------------------------------------------------------------- /lib/EntryPoint.php: -------------------------------------------------------------------------------- 1 | url = $url; 38 | $this->client = $client; 39 | $this->headers = $headers; 40 | 41 | $this->resource = false; 42 | } 43 | 44 | /** 45 | * @param HttpResponse $response 46 | * @param HttpClientInterface $client 47 | * 48 | * @return Resource 49 | * 50 | * @throws \RuntimeException 51 | */ 52 | public static function parse(HttpResponse $response, HttpClientInterface $client) 53 | { 54 | if (substr($response->getHeader('Content-Type'), 0, 20) !== 'application/hal+json') { 55 | throw new \RuntimeException('Invalid content type'); 56 | } 57 | 58 | $data = @json_decode($response->getBody(), true); 59 | 60 | if ($data === null) { 61 | throw new \RuntimeException('Invalid JSON format'); 62 | } 63 | 64 | return Resource::create($client, $data); 65 | } 66 | 67 | /** 68 | * @param string $name 69 | * 70 | * @return Resource 71 | */ 72 | public function get($name = null) 73 | { 74 | $this->initialize(); 75 | 76 | if ($name) { 77 | return $this->resource->get($name); 78 | } 79 | 80 | return $this->resource; 81 | } 82 | 83 | /** 84 | * Initialize the resource. 85 | */ 86 | protected function initialize() 87 | { 88 | if ($this->resource) { 89 | return; 90 | } 91 | 92 | $this->resource = static::parse($this->client->get($this->url), $this->client); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/Exception/RequestException.php: -------------------------------------------------------------------------------- 1 | defaultHeaders = $defaultHeaders; 44 | $this->timeout = $timeout; 45 | $this->proxy = $proxy; 46 | 47 | // normalize 48 | if (substr($baseUrl, -1) !== '/') { 49 | $baseUrl .= '/'; 50 | } 51 | 52 | $this->baseUrl = $baseUrl; 53 | } 54 | 55 | /** 56 | * @param string $url 57 | * @param string $method 58 | * @param array $headers 59 | * @param array $data 60 | * 61 | * @return HttpResponse 62 | * 63 | * @throws RequestException 64 | */ 65 | protected function doRequest($url, $method, array $headers, array $data) 66 | { 67 | $headers['Accept'] = 'application/hal+json'; 68 | 69 | $opts = array( 70 | 'http' => array( 71 | 'method' => strtoupper($method), 72 | 'header' => $this->buildHeaders(array_merge($this->defaultHeaders, $headers)), 73 | 'timeout' => $this->timeout, 74 | 'ignore_errors' => true, 75 | 76 | // need to set configuration options 77 | 'user_agent' => 'Ekino HalClient v0.1', 78 | 'follow_location' => 0, 79 | 'max_redirects' => 20, 80 | ) 81 | ); 82 | 83 | if ($this->proxy) { 84 | $opts['http']['proxy'] = $this->proxy; 85 | $opts['http']['request_fulluri'] = true; 86 | } 87 | 88 | // if is relative url 89 | if ('http' !== substr($url, 0, 4)) { 90 | // clean 91 | if ($url[0] === '/') { 92 | $url = substr($url, 1); 93 | } 94 | 95 | $url = $this->baseUrl . $url; 96 | } 97 | 98 | // http://php.net/manual/en/reserved.variables.httpresponseheader.php 99 | $content = @file_get_contents($url, false, stream_context_create($opts)); 100 | 101 | if (empty($http_response_header) && $content === false) { 102 | throw new RequestException('Empty response, no headers or impossible to reach the remote server'); 103 | } 104 | 105 | $data = explode(" ", $http_response_header[0]); 106 | 107 | return new HttpResponse($data[1], $this->parseHeaders($http_response_header), $content); 108 | } 109 | 110 | /** 111 | * @param array $headers 112 | * 113 | * @return string 114 | */ 115 | protected function buildHeaders(array $headers) 116 | { 117 | $data = ""; 118 | foreach ($headers as $name => $value) { 119 | $data .= sprintf("%s: %s\r\n", $name, $value); 120 | } 121 | 122 | return $data; 123 | } 124 | 125 | /** 126 | * @param $lines 127 | * 128 | * @return array 129 | */ 130 | protected function parseHeaders($lines) 131 | { 132 | $headers = array(); 133 | foreach($lines as $line) { 134 | $data = explode(":", $line, 2); 135 | 136 | if (count($data) !== 2) { 137 | continue; 138 | } 139 | 140 | $headers[$data[0]] = trim($data[1]); 141 | } 142 | 143 | return $headers; 144 | } 145 | 146 | /** 147 | * {@inheritdoc} 148 | */ 149 | public function get($url, $headers = array()) 150 | { 151 | return $this->doRequest($url, 'GET', $headers, array()); 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function post($url, $data = array(), $headers = array()) 158 | { 159 | throw new \RuntimeException('Feature not implemented'); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function put($url, $data = array(), $headers = array()) 166 | { 167 | throw new \RuntimeException('Feature not implemented'); 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function delete($url, $headers = array()) 174 | { 175 | throw new \RuntimeException('Feature not implemented'); 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function patch($url, $data = array(), $headers = array()) 182 | { 183 | throw new \RuntimeException('Feature not implemented'); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/HttpClient/HttpClientInterface.php: -------------------------------------------------------------------------------- 1 | status = (int) $status; 30 | $this->headers = $headers; 31 | $this->body = $body; 32 | } 33 | 34 | /** 35 | * @return int 36 | */ 37 | public function getStatus() 38 | { 39 | return $this->status; 40 | } 41 | 42 | /** 43 | * @return mixed 44 | */ 45 | public function getBody() 46 | { 47 | return $this->body; 48 | } 49 | 50 | /** 51 | * @return mixed 52 | */ 53 | public function getHeaders() 54 | { 55 | return $this->headers; 56 | } 57 | 58 | /** 59 | * @param string $name 60 | * @param mixed $default 61 | * 62 | * @return mixed 63 | */ 64 | public function getHeader($name, $default = null) 65 | { 66 | if (!array_key_exists($name, $this->headers)) { 67 | return $default; 68 | } 69 | 70 | return $this->headers[$name]; 71 | } 72 | } -------------------------------------------------------------------------------- /lib/Link.php: -------------------------------------------------------------------------------- 1 | title = isset($data['title']) ? $data['title'] : null; 46 | 47 | if (null !== $this->name && false !== strpos($this->name, ':')) { 48 | list($this->ncName, $this->reference) = explode(':', $this->name, 2); 49 | } 50 | } 51 | 52 | /** 53 | * @return null|string 54 | */ 55 | public function getNCName() 56 | { 57 | return $this->ncName; 58 | } 59 | 60 | /** 61 | * @return null|string 62 | */ 63 | public function getReference() 64 | { 65 | return $this->reference; 66 | } 67 | 68 | /** 69 | * @return null|string 70 | */ 71 | public function getTitle() 72 | { 73 | return $this->title; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/Proxy/HalResourceEntity.php: -------------------------------------------------------------------------------- 1 | __halResourceLoaded = $halResourceLoaded; 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | public function getHalResourceLoaded() 46 | { 47 | return $this->__halResourceLoaded; 48 | } 49 | 50 | /** 51 | * @param mixed $halSerializer 52 | */ 53 | public function setHalSerializer(Serializer $halSerializer) 54 | { 55 | $this->__halSerializer = $halSerializer; 56 | } 57 | 58 | /** 59 | * @return mixed 60 | */ 61 | public function getHalSerializer() 62 | { 63 | return $this->__halSerializer; 64 | } 65 | 66 | /** 67 | * @param Resource $halResource 68 | */ 69 | public function setHalResource(Resource $halResource) 70 | { 71 | $this->__halResource = $halResource; 72 | } 73 | 74 | /** 75 | * @return Resource 76 | */ 77 | public function getHalResource() 78 | { 79 | return $this->__halResource; 80 | } 81 | 82 | /** 83 | * @param $name 84 | */ 85 | public function halLoaded($name) 86 | { 87 | $this->__halResourceLoaded[$name] = true; 88 | } 89 | 90 | /** 91 | * @param $name 92 | * 93 | * @return bool 94 | */ 95 | public function halIsLoaded($name) 96 | { 97 | return isset($this->__halResourceLoaded[$name]); 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /lib/Proxy/HalResourceEntityInterface.php: -------------------------------------------------------------------------------- 1 | client = $client; 38 | $this->properties = $properties; 39 | $this->links = $links; 40 | $this->embedded = $embedded; 41 | 42 | $this->parseCuries(); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function getEmbedded() 49 | { 50 | return $this->embedded; 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function getLinks() 57 | { 58 | return $this->links; 59 | } 60 | 61 | /** 62 | * Reloads the Resource by using the self reference 63 | * 64 | * @throws \RuntimeException 65 | */ 66 | public function refresh() 67 | { 68 | $link = $this->getLink('self'); 69 | 70 | if (!$link) { 71 | throw new \RuntimeException('Invalid resource, not `self` reference available'); 72 | } 73 | 74 | $r = $this->getResource($link); 75 | 76 | $this->properties = $r->getProperties(); 77 | $this->links = $r->getLinks(); 78 | $this->embedded = $r->getEmbedded(); 79 | 80 | $this->parseCuries(); 81 | } 82 | 83 | /** 84 | * @param $name 85 | * 86 | * @return Link 87 | */ 88 | public function getLink($name) 89 | { 90 | if (!array_key_exists($name, $this->links)) { 91 | return null; 92 | } 93 | 94 | if (!$this->links[$name] instanceof Link) { 95 | $this->links[$name] = new Link(array_merge(array('name' => $name), $this->links[$name])); 96 | } 97 | 98 | return $this->links[$name]; 99 | } 100 | 101 | /** 102 | * @param $name 103 | * 104 | * @return Curie 105 | */ 106 | public function getCurie($name) 107 | { 108 | if (!array_key_exists($name, $this->curies)) { 109 | return null; 110 | } 111 | 112 | return $this->curies[$name]; 113 | } 114 | 115 | /** 116 | * @return array 117 | */ 118 | public function getProperties() 119 | { 120 | return $this->properties; 121 | } 122 | 123 | protected function parseCuries() 124 | { 125 | $this->curies = array(); 126 | 127 | if (!array_key_exists('curies', $this->links)) { 128 | return; 129 | } 130 | 131 | foreach ($this->links['curies'] as $curie) { 132 | $this->curies[$curie['name']] = new Curie($curie); 133 | } 134 | } 135 | 136 | /** 137 | * @param $name 138 | * 139 | * @return Resource|ResourceCollection|null 140 | */ 141 | public function get($name) 142 | { 143 | if (array_key_exists($name, $this->properties)) { 144 | return $this->properties[$name]; 145 | } 146 | 147 | if (!array_key_exists($name, $this->embedded)) { 148 | if (!$this->buildResourceValue($name)) { 149 | return null; 150 | } 151 | } 152 | 153 | return $this->getEmbeddedValue($name); 154 | } 155 | 156 | /** 157 | * @param $name 158 | * 159 | * @return bool 160 | */ 161 | public function has($name) 162 | { 163 | return $this->hasProperty($name) || $this->hasLink($name) || $this->hasEmbedded($name); 164 | } 165 | 166 | /** 167 | * @param $name 168 | * 169 | * @return bool 170 | */ 171 | public function hasLink($name) 172 | { 173 | return isset($this->links[$name]); 174 | } 175 | 176 | /** 177 | * @param $name 178 | * 179 | * @return bool 180 | */ 181 | public function hasProperty($name) 182 | { 183 | return isset($this->properties[$name]); 184 | } 185 | 186 | /** 187 | * @param $name 188 | * 189 | * @return bool 190 | */ 191 | public function hasEmbedded($name) 192 | { 193 | return isset($this->embedded[$name]); 194 | } 195 | 196 | /** 197 | * @param $name 198 | * 199 | * @return boolean 200 | */ 201 | protected function buildResourceValue($name) 202 | { 203 | $link = $this->getLink($name); 204 | 205 | if (!$link) { 206 | return false; 207 | } 208 | 209 | $this->embedded[$name] = $this->getResource($link); 210 | 211 | return true; 212 | } 213 | 214 | /** 215 | * @param $name 216 | * 217 | * @return Resource|ResourceCollection 218 | */ 219 | protected function getEmbeddedValue($name) 220 | { 221 | if ( !is_object($this->embedded[$name])) { 222 | if (is_integer(key($this->embedded[$name])) || empty($this->embedded[$name])) { 223 | $this->embedded[$name] = new ResourceCollection($this->client, $this->embedded[$name]); 224 | } else { 225 | $this->embedded[$name] = self::create($this->client, $this->embedded[$name]); 226 | } 227 | } 228 | 229 | return $this->embedded[$name]; 230 | } 231 | 232 | /** 233 | * @param HttpClientInterface $client 234 | * @param array $data 235 | * 236 | * @return Resource 237 | */ 238 | public static function create(HttpClientInterface $client, array $data) 239 | { 240 | $links = isset($data['_links']) ? $data['_links'] : array(); 241 | $embedded = isset($data['_embedded']) ? $data['_embedded'] : array(); 242 | 243 | unset( 244 | $data['_links'], 245 | $data['_embedded'] 246 | ); 247 | 248 | return new self($client, $data, $links, $embedded); 249 | } 250 | 251 | /** 252 | * Create a resource from link href. 253 | * 254 | * @param Link $link 255 | * @param array $variables Required if the link is templated 256 | * 257 | * @return Resource 258 | * 259 | * @throws \RuntimeException When call with property "href" of Link is empty and sets variables 260 | * Or response server is invalid 261 | */ 262 | public function getResource(Link $link, array $variables = array()) 263 | { 264 | $response = $this->client->get($link->getHref($variables)); 265 | 266 | if (!$response instanceof HttpResponse) { 267 | throw new \RuntimeException(sprintf('HttpClient does not return a valid HttpResponse object, given: %s', $response)); 268 | } 269 | 270 | if ($response->getStatus() !== 200) { 271 | throw new \RuntimeException(sprintf('HttpClient does not return a status code, given: %s', $response->getStatus())); 272 | } 273 | 274 | return EntryPoint::parse($response, $this->client); 275 | } 276 | 277 | /** 278 | * Returns the href of curie assoc given by link. 279 | * 280 | * @param Link $link 281 | * 282 | * @return string 283 | */ 284 | public function getCurieHref(Link $link) 285 | { 286 | if (null === $link->getNCName() || null === $this->getCurie($link->getNCName())) { 287 | return null; 288 | } 289 | 290 | return $this->getCurie($link->getNCName())->getHref(array('rel' => $link->getReference())); 291 | } 292 | 293 | /** 294 | * {@inheritdoc} 295 | */ 296 | public function offsetExists($offset) 297 | { 298 | return $this->has($offset); 299 | } 300 | 301 | /** 302 | * {@inheritdoc} 303 | */ 304 | public function offsetGet($offset) 305 | { 306 | return $this->get($offset); 307 | } 308 | 309 | /** 310 | * {@inheritdoc} 311 | */ 312 | public function offsetSet($offset, $value) 313 | { 314 | throw new \RuntimeException('Operation not available'); 315 | } 316 | 317 | /** 318 | * {@inheritdoc} 319 | */ 320 | public function offsetUnset($offset) 321 | { 322 | throw new \RuntimeException('Operation not available'); 323 | } 324 | } -------------------------------------------------------------------------------- /lib/ResourceCollection.php: -------------------------------------------------------------------------------- 1 | client = $client; 31 | $this->iterator = new \ArrayIterator($collection); 32 | } 33 | 34 | /** 35 | * @param HttpClientInterface $client 36 | * @param \Iterator $collection 37 | * @param bool $updateIterator if the Iterator should be updated to wrap the data inside Resource instances 38 | * 39 | * @return ResourceCollection 40 | */ 41 | public static function createFromIterator(HttpClientInterface $client, \Iterator $collection, $updateIterator = false) 42 | { 43 | $col = new self($client); 44 | $col->iterator = $collection; 45 | $col->updateIterator = $updateIterator; 46 | 47 | return $col; 48 | } 49 | 50 | /** 51 | * @param null|array $data 52 | * 53 | * @return Resource 54 | */ 55 | protected function createResource($data) 56 | { 57 | if (null === $data) { 58 | return null; 59 | } 60 | 61 | return Resource::create($this->client, $data); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function current() 68 | { 69 | $resource = $this->iterator->current(); 70 | if (null === $resource) { 71 | return null; 72 | } 73 | 74 | if ($this->updateIterator && !$resource instanceof Resource) { 75 | $resource = $this->createResource($resource); 76 | $this->iterator->offsetSet($this->iterator->key(), $resource); 77 | } 78 | 79 | return $resource; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function next() 86 | { 87 | $this->iterator->next(); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function key() 94 | { 95 | return $this->iterator->key(); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function valid() 102 | { 103 | return $this->iterator->valid(); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function rewind() 110 | { 111 | $this->iterator->rewind(); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function count() 118 | { 119 | if (!$this->iterator instanceof \Countable) { 120 | throw new \RuntimeException('Operation not allowed'); 121 | } 122 | 123 | return count($this->iterator); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function offsetExists($offset) 130 | { 131 | if (!$this->iterator instanceof \ArrayAccess) { 132 | throw new \RuntimeException('Operation not allowed'); 133 | } 134 | 135 | return isset($this->iterator[$offset]); 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function offsetGet($offset) 142 | { 143 | if (!$this->iterator instanceof \ArrayAccess) { 144 | throw new \RuntimeException('Operation not allowed'); 145 | } 146 | 147 | $resource = $this->iterator->offsetGet($offset); 148 | if (null === $resource) { 149 | return null; 150 | } 151 | 152 | if ($this->updateIterator && !$resource instanceof Resource) { 153 | $resource = $this->createResource($resource); 154 | $this->iterator->offsetSet($offset, $resource); 155 | } 156 | 157 | return $resource; 158 | } 159 | 160 | /** 161 | * {@inheritdoc} 162 | */ 163 | public function offsetSet($offset, $value) 164 | { 165 | throw new \RuntimeException('Operation not allowed'); 166 | } 167 | 168 | /** 169 | * {@inheritdoc} 170 | */ 171 | public function offsetUnset($offset) 172 | { 173 | throw new \RuntimeException('Operation not allowed'); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/HalClient/ 7 | 8 | 9 | 10 | 11 | 12 | ./ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/HalClient/Deserialization/Construction/ProxyObjectConstructionTest.php: -------------------------------------------------------------------------------- 1 | getMock('JMS\Serializer\VisitorInterface'); 24 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 25 | 26 | $context = new DeserializationContext(); 27 | $resource = new Resource($client); 28 | 29 | $constructor = new ProxyObjectConstruction(); 30 | $object = $constructor->construct($visitor, new ClassMetadata('Acme\Post'), $resource, array(), $context); 31 | 32 | $this->assertInstanceOf('Ekino\HalClient\Proxy\HalResourceEntityInterface', $object); 33 | $this->assertInstanceOf('Ekino\HalClient\Resource', $object->getHalResource()); 34 | $this->assertInstanceOf('Acme\Post', $object); 35 | } 36 | 37 | public function testConstructorWithoutProxy() 38 | { 39 | $visitor = $this->getMock('JMS\Serializer\VisitorInterface'); 40 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 41 | $context = new DeserializationContext(); 42 | $resource = new Resource($client); 43 | 44 | $constructor = new ProxyObjectConstruction(); 45 | $object = $constructor->construct($visitor, new ClassMetadata('Acme\NoProxy'), $resource, array(), $context); 46 | 47 | $this->assertNotInstanceOf('Ekino\HalClient\Proxy\HalResourceEntityInterface', $object); 48 | $this->assertInstanceOf('Acme\NoProxy', $object); 49 | } 50 | 51 | public function testWithMultiplePatterns() 52 | { 53 | $visitor = $this->getMock('JMS\Serializer\VisitorInterface'); 54 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 55 | $context = new DeserializationContext(); 56 | $resource = new Resource($client); 57 | 58 | $constructor = new ProxyObjectConstruction(["{ns}\\Proxy\\{ln}", "Proxy\\{ns}\\{ln}"]); 59 | $object = $constructor->construct($visitor, new ClassMetadata('Acme\Post'), $resource, array(), $context); 60 | 61 | $this->assertInstanceOf('Ekino\HalClient\Proxy\HalResourceEntityInterface', $object); 62 | $this->assertInstanceOf('Ekino\HalClient\Resource', $object->getHalResource()); 63 | $this->assertInstanceOf('Proxy\Acme\Post', $object); 64 | } 65 | } 66 | } 67 | 68 | namespace Acme 69 | { 70 | class Post {} 71 | 72 | class NoProxy {} 73 | } 74 | 75 | namespace Proxy\Acme 76 | { 77 | use Ekino\HalClient\Proxy\HalResourceEntity; 78 | use Ekino\HalClient\Proxy\HalResourceEntityInterface; 79 | 80 | class Post extends \Acme\Post implements HalResourceEntityInterface 81 | { 82 | use HalResourceEntity; 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /tests/HalClient/Deserialization/DeserializationTest.php: -------------------------------------------------------------------------------- 1 | getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 32 | $client->expects($this->exactly(1))->method('get')->will($this->returnCallback(function($url) { 33 | if ($url == '/users/1') { 34 | return new HttpResponse(200, array( 35 | 'Content-Type' => 'application/hal+json' 36 | ), json_encode(array( 37 | 'name' => 'Thomas Rabaix', 38 | 'email' => 'thomas.rabaix@ekino.com' 39 | ))); 40 | } 41 | })); 42 | 43 | $resource = new Resource($client, array( 44 | 'name' => 'Salut', 45 | ), array( 46 | 'fragments' => array('href' => '/document/1/fragments'), 47 | 'author' => array('href' => '/users/1') 48 | ), array( 49 | 'fragments' => array( 50 | array( 51 | 'type' => 'test', 52 | 'settings' => array( 53 | 'color' => 'red' 54 | ) 55 | ), 56 | array( 57 | 'type' => 'image', 58 | 'settings' => array( 59 | 'url' => 'http://dummyimage.com/600x400/000/fff' 60 | ) 61 | ) 62 | ) 63 | )); 64 | 65 | return $resource; 66 | } 67 | 68 | public function testMapping() 69 | { 70 | $resource = $this->getResource(); 71 | 72 | $object = Builder::build()->deserialize($resource, 'Ekino\HalClient\Deserialization\Article', 'hal'); 73 | 74 | $this->assertEquals('Salut', $object->getName()); 75 | 76 | $fragments = $object->getFragments(); 77 | $this->assertCount(2, $fragments); 78 | $this->assertEquals($fragments[0]->getType(), 'test'); 79 | $this->assertEquals($fragments[1]->getType(), 'image'); 80 | $this->assertNotNull($object->getAuthor()); 81 | $this->assertEquals($object->getAuthor()->getEmail(), 'thomas.rabaix@ekino.com'); 82 | } 83 | 84 | public function testWithValidProxy() 85 | { 86 | $serializerBuilder = Builder::get(false) 87 | ->setObjectConstructor($constructor = new ProxyObjectConstruction()); 88 | 89 | // todo, inject proxy handler 90 | $serializerBuilder->setObjectConstructor($constructor); 91 | 92 | $resource = $this->getResource(); 93 | $serializer = $serializerBuilder->build(); 94 | 95 | $constructor->setSerializer($serializer); 96 | 97 | $object = $serializer->deserialize($resource, 'Ekino\HalClient\Deserialization\Article', 'hal'); 98 | 99 | $this->assertInstanceOf('Proxy\Ekino\HalClient\Deserialization\Article', $object); 100 | $this->assertInstanceOf('Ekino\HalClient\Deserialization\Article', $object); 101 | $this->assertEquals('Salut', $object->getName()); 102 | 103 | $fragments = $object->getFragments(); 104 | 105 | $this->assertCount(2, $fragments); 106 | $this->assertEquals($fragments[0]->getType(), 'test'); 107 | $this->assertEquals($fragments[1]->getType(), 'image'); 108 | 109 | $this->assertInstanceOf('Proxy\Ekino\HalClient\Deserialization\Author', $object->getAuthor()); 110 | $this->assertInstanceOf('Ekino\HalClient\Deserialization\Author', $object->getAuthor()); 111 | 112 | $this->assertEquals($object->getAuthor()->getEmail(), 'thomas.rabaix@ekino.com'); 113 | } 114 | } 115 | } 116 | 117 | namespace Proxy\Ekino\HalClient\Deserialization { 118 | 119 | use Ekino\HalClient\Proxy\HalResourceEntity; 120 | use Ekino\HalClient\Proxy\HalResourceEntityInterface; 121 | use Ekino\HalClient\Resource; 122 | 123 | class Article extends \Ekino\HalClient\Deserialization\Article implements HalResourceEntityInterface 124 | { 125 | use HalResourceEntity; 126 | 127 | /** 128 | * @return Author 129 | */ 130 | public function getAuthor() 131 | { 132 | if (!$this->author && !$this->halIsLoaded('author')) { 133 | $this->halLoaded('author'); 134 | 135 | $resource = $this->getHalResource()->get('author'); 136 | 137 | if ($resource instanceof Resource) { 138 | $this->author = $this->getHalSerializer()->deserialize($resource, 'Ekino\HalClient\Deserialization\Author', 'hal'); 139 | } 140 | } 141 | 142 | return $this->author; 143 | } 144 | } 145 | 146 | class Author extends \Ekino\HalClient\Deserialization\Author implements HalResourceEntityInterface 147 | { 148 | use HalResourceEntity; 149 | } 150 | } 151 | 152 | namespace Ekino\HalClient\Deserialization { 153 | use JMS\Serializer\Annotation as Serializer; 154 | 155 | class Article 156 | { 157 | /** 158 | * @Serializer\Type("string") 159 | * 160 | * @var string 161 | */ 162 | protected $name; 163 | 164 | /** 165 | * @Serializer\Type("Doctrine\Common\Collections\ArrayCollection") 166 | * 167 | * @var array 168 | */ 169 | protected $fragments; 170 | 171 | /** 172 | * @param array $author 173 | */ 174 | public function setAuthor(Author $author) 175 | { 176 | $this->author = $author; 177 | } 178 | 179 | /** 180 | * @return array 181 | */ 182 | public function getAuthor() 183 | { 184 | return $this->author; 185 | } 186 | 187 | /** 188 | * @Serializer\Type("Ekino\HalClient\Deserialization\Author") 189 | * 190 | * @var array 191 | */ 192 | protected $author; 193 | 194 | /** 195 | * @param array $fragments 196 | */ 197 | public function setFragments(ArrayCollection $fragments) 198 | { 199 | $this->fragments = $fragments; 200 | } 201 | 202 | /** 203 | * @return array 204 | */ 205 | public function getFragments() 206 | { 207 | return $this->fragments; 208 | } 209 | 210 | /** 211 | * @param string $name 212 | */ 213 | public function setName($name) 214 | { 215 | $this->name = $name; 216 | } 217 | 218 | /** 219 | * @return string 220 | */ 221 | public function getName() 222 | { 223 | return $this->name; 224 | } 225 | } 226 | 227 | class Fragment 228 | { 229 | /** 230 | * @Serializer\Type("string") 231 | * 232 | * @var string 233 | */ 234 | protected $type; 235 | 236 | /** 237 | * @Serializer\Type("array") 238 | * 239 | * @var array 240 | */ 241 | protected $settings; 242 | 243 | /** 244 | * @param array $settings 245 | */ 246 | public function setSettings($settings) 247 | { 248 | $this->settings = $settings; 249 | } 250 | 251 | /** 252 | * @return array 253 | */ 254 | public function getSettings() 255 | { 256 | return $this->settings; 257 | } 258 | 259 | /** 260 | * @param string $type 261 | */ 262 | public function setType($type) 263 | { 264 | $this->type = $type; 265 | } 266 | 267 | /** 268 | * @return string 269 | */ 270 | public function getType() 271 | { 272 | return $this->type; 273 | } 274 | } 275 | 276 | class Author 277 | { 278 | /** 279 | * @Serializer\Type("string") 280 | * 281 | * @var string 282 | */ 283 | protected $name; 284 | 285 | /** 286 | * @Serializer\Type("string") 287 | * 288 | * @var string 289 | */ 290 | protected $email; 291 | 292 | /** 293 | * @param string $email 294 | */ 295 | public function setEmail($email) 296 | { 297 | $this->email = $email; 298 | } 299 | 300 | /** 301 | * @return string 302 | */ 303 | public function getEmail() 304 | { 305 | return $this->email; 306 | } 307 | 308 | /** 309 | * @param string $name 310 | */ 311 | public function setName($name) 312 | { 313 | $this->name = $name; 314 | } 315 | 316 | /** 317 | * @return string 318 | */ 319 | public function getName() 320 | { 321 | return $this->name; 322 | } 323 | } 324 | } -------------------------------------------------------------------------------- /tests/HalClient/EntryPointTest.php: -------------------------------------------------------------------------------- 1 | getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 26 | $client->expects($this->once())->method('get')->will($this->returnValue(new HttpResponse(200, array( 27 | 'Content-Type' => 'application/json' 28 | )), '{}')); 29 | 30 | $entryPoint = new EntryPoint('/', $client); 31 | 32 | $entryPoint->get(); 33 | } 34 | 35 | public function testVersionHeader() 36 | { 37 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 38 | $client->expects($this->once())->method('get')->will($this->returnCallback(function($url) { 39 | return new HttpResponse(200, array( 40 | 'Content-Type' => 'application/hal+json;version=42' 41 | ), json_encode([])); 42 | })); 43 | 44 | (new EntryPoint('/', $client))->get(); 45 | } 46 | 47 | public function testGetResource() 48 | { 49 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 50 | $client->expects($this->any())->method('get')->will($this->returnCallback(function($url) { 51 | 52 | if ($url === '/') { 53 | return new HttpResponse(200, array( 54 | 'Content-Type' => 'application/hal+json' 55 | ), file_get_contents(__DIR__.'/../fixtures/entry_point.json')); 56 | } 57 | 58 | if ($url === 'http://propilex.herokuapp.com/documents') { 59 | return new HttpResponse(200, array( 60 | 'Content-Type' => 'application/hal+json' 61 | ), file_get_contents(__DIR__.'/../fixtures/documents.json')); 62 | } 63 | })); 64 | 65 | $entryPoint = new EntryPoint('/', $client); 66 | 67 | $resource = $entryPoint->get(); 68 | 69 | $this->assertInstanceOf('Ekino\HalClient\Resource', $resource); 70 | $this->assertCount(1, $resource->getProperties()); 71 | $this->assertEmpty($resource->getEmbedded()); 72 | 73 | $link = $resource->getLink('p:documents'); 74 | 75 | $this->assertInstanceOf('Ekino\HalClient\Link', $link); 76 | 77 | $this->assertEquals($link->getHref(), 'http://propilex.herokuapp.com/documents'); 78 | 79 | $this->assertNull($resource->get('fake')); 80 | 81 | $resource = $resource->get('p:documents'); 82 | 83 | $this->assertInstanceOf('Ekino\HalClient\Resource', $resource); 84 | 85 | $expected = array( 86 | "page" => 1, 87 | "limit" => 10, 88 | "pages" => 1, 89 | ); 90 | 91 | $this->assertEquals($expected, $resource->getProperties()); 92 | $this->assertEquals(1, $resource->get('page')); 93 | $this->assertEquals(10, $resource->get('limit')); 94 | $this->assertEquals(1, $resource->get('pages')); 95 | 96 | $collection = $resource->get('documents'); 97 | 98 | $this->assertInstanceOf('Ekino\HalClient\ResourceCollection', $collection); 99 | 100 | $this->assertEquals(4, $collection->count()); 101 | 102 | foreach ($collection as $child) { 103 | $this->assertInstanceOf('Ekino\HalClient\Resource', $child); 104 | $this->assertNotNull($child->get('title')); 105 | $this->assertNotNull($child->get('body')); 106 | $this->assertNotNull($child->get('id')); 107 | $this->assertNull($child->get('fake')); 108 | } 109 | 110 | 111 | $this->assertEquals('teste', $collection[1]->get('title')); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/HalClient/HttpClient/FileGetContentsHttpClientTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 23 | 24 | $this->assertInstanceOf('Ekino\HalClient\HttpClient\HttpResponse', $response); 25 | 26 | $this->assertEquals(200, $response->getStatus()); 27 | $this->assertNotNull($response->getHeader('Content-Type')); 28 | 29 | $this->assertContains('Example Domain', $response->getBody()); 30 | } 31 | 32 | public function testGet404() 33 | { 34 | $client = new FileGetContentsHttpClient('http://www.google.com/this-is-an-invalid-url', array(), 5.0); 35 | 36 | $response = $client->get('/'); 37 | 38 | $this->assertEquals($response->getStatus(), 404); 39 | } 40 | 41 | /** 42 | * @expectedException \Ekino\HalClient\Exception\RequestException 43 | * @expectedExceptionMessage Empty response, no headers or impossible to reach the remote server 44 | */ 45 | public function testGetUnreachable() 46 | { 47 | $client = new FileGetContentsHttpClient('https://8.8.8.8/', array(), 2.0); // Google DNS ;) 48 | 49 | $response = $client->get('/'); 50 | 51 | $this->assertEquals($response->getStatus(), 404); 52 | } 53 | } -------------------------------------------------------------------------------- /tests/HalClient/LinkTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Ekino\\HalClient\\Link', $this->resource->getLink('test:media')); 29 | 30 | $this->assertEquals('test', $this->resource->getLink('test:media')->getNCName()); 31 | $this->assertEquals('media', $this->resource->getLink('test:media')->getReference()); 32 | $this->assertEquals('test:media', $this->resource->getLink('test:media')->getName()); 33 | 34 | $this->assertInstanceOf('Ekino\\HalClient\\Link', $this->resource->getLink('article')); 35 | 36 | $this->assertNull($this->resource->getLink('article')->getNCName()); 37 | $this->assertNull($this->resource->getLink('article')->getReference()); 38 | $this->assertEquals('article', $this->resource->getLink('article')->getName()); 39 | 40 | $this->assertInstanceOf('Ekino\\HalClient\\Link', $this->resource->getLink('bar:tag')); 41 | 42 | $this->assertEquals('bar', $this->resource->getLink('bar:tag')->getNCName()); 43 | $this->assertEquals('tag', $this->resource->getLink('bar:tag')->getReference()); 44 | $this->assertEquals('bar:tag', $this->resource->getLink('bar:tag')->getName()); 45 | } 46 | 47 | public function testGetCurie() 48 | { 49 | $this->assertInstanceOf('Ekino\\HalClient\\Curie', $this->resource->getCurie('test')); 50 | $this->assertNull($this->resource->getCurie('bar')); 51 | 52 | $this->assertEquals('test', $this->resource->getCurie('test')->getName()); 53 | $this->assertEquals('http://localhost/path/to/docs/{rel}', $this->resource->getCurie('test')->getHref()); 54 | $this->assertTrue($this->resource->getCurie('test')->isTemplated()); 55 | } 56 | 57 | public function testGetDocs() 58 | { 59 | $this->assertEquals('http://localhost/path/to/docs/media', $this->resource->getCurieHref($this->resource->getLink('test:media'))); 60 | 61 | $this->assertNull($this->resource->getCurieHref($this->resource->getLink('article'))); 62 | 63 | $this->assertNull($this->resource->getCurieHref($this->resource->getLink('bar:tag'))); 64 | } 65 | 66 | /** 67 | * @expectedException \RuntimeException 68 | */ 69 | public function testLinkGetFail() 70 | { 71 | $this->resource->getResource($this->resource->getLink('article')); 72 | } 73 | 74 | public function testLinkHref() 75 | { 76 | $this->assertEquals('http://localhost/article/42', $this->resource->getLink('article')->getHref(array('id' => 42))); 77 | 78 | $this->assertEquals('http://localhost/tag/{id}', $this->resource->getLink('bar:tag')->getHref(array('id' => 42))); 79 | 80 | $this->assertEquals('http://localhost/media', $this->resource->getLink('test:media')->getHref()); 81 | } 82 | 83 | protected function setUp() 84 | { 85 | $this->client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 86 | 87 | $this->resource = new Resource($this->client, array(), array( 88 | 'curies' => array( 89 | array( 90 | 'name' => 'test', 91 | 'href' => 'http://localhost/path/to/docs/{rel}', 92 | 'templated' => true 93 | ) 94 | ), 95 | 'test:media' => array( 96 | 'href' => 'http://localhost/media' 97 | ), 98 | 'article' => array( 99 | 'href' => 'http://localhost/article/{id}', 100 | 'templated' => true 101 | ), 102 | 'bar:tag' => array( 103 | 'href' => 'http://localhost/tag/{id}', 104 | 'templated' => false 105 | ) 106 | )); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/HalClient/ResourceTest.php: -------------------------------------------------------------------------------- 1 | getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 23 | 24 | $resource = new Resource($client); 25 | } 26 | 27 | /** 28 | * @expectedException \RuntimeException 29 | * @expectedExceptionMessage HttpClient does not return a status code, given: 500 30 | */ 31 | public function testInvalidStatus() 32 | { 33 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 34 | $client->expects($this->once())->method('get')->will($this->returnValue(new HttpResponse(500))); 35 | 36 | $resource = new Resource($client, array(), array( 37 | 'foo' => array( 38 | 'href' => 'http://fake.com/foo', 39 | 'title' => 'foo' 40 | ) 41 | )); 42 | 43 | $resource->get('foo'); 44 | } 45 | 46 | /** 47 | * @expectedException \RuntimeException 48 | * @expectedExceptionMessage Invalid resource, not `self` reference available 49 | */ 50 | public function testRefreshWithInvalidSelfReference() 51 | { 52 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 53 | $client->expects($this->never())->method('get'); 54 | 55 | $resource = new Resource($client); 56 | 57 | $resource->refresh(); 58 | } 59 | 60 | public function testRefresh() 61 | { 62 | $client = $this->getMock('Ekino\HalClient\HttpClient\HttpClientInterface'); 63 | $client->expects($this->exactly(1))->method('get')->will($this->returnCallback(function($url) { 64 | if ($url == 'http://propilex.herokuapp.com') { 65 | return new HttpResponse(200, array( 66 | 'Content-Type' => 'application/hal+json' 67 | ), file_get_contents(__DIR__.'/../fixtures/entry_point.json')); 68 | } 69 | })); 70 | 71 | $resource = new Resource($client, array(), array( 72 | 'self' => array('href'=>'http://propilex.herokuapp.com') 73 | )); 74 | 75 | $this->assertNull($resource->get('field')); 76 | $resource->refresh(); 77 | 78 | $this->assertEquals($resource->get('field'), 'value'); 79 | } 80 | } -------------------------------------------------------------------------------- /tests/autoload.php.dist: -------------------------------------------------------------------------------- 1 | getFilename(), 0, -34) 32 | ); 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |