1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Json\Server\Smd; 11 12use Zend\Json\Server\Exception\InvalidArgumentException; 13use Zend\Json\Server\Smd; 14 15/** 16 * Create Service Mapping Description for a method 17 * 18 * @todo Revised method regex to allow NS; however, should SMD be revised to strip PHP NS instead when attaching functions? 19 */ 20class Service 21{ 22 /**#@+ 23 * Service metadata 24 * @var string 25 */ 26 protected $envelope = Smd::ENV_JSONRPC_1; 27 protected $name; 28 protected $return; 29 protected $target; 30 protected $transport = 'POST'; 31 /**#@-*/ 32 33 /** 34 * Allowed envelope types 35 * @var array 36 */ 37 protected $envelopeTypes = array( 38 Smd::ENV_JSONRPC_1, 39 Smd::ENV_JSONRPC_2, 40 ); 41 42 /** 43 * Regex for names 44 * @var string 45 * 46 * @link http://php.net/manual/en/language.oop5.basic.php 47 * @link http://www.jsonrpc.org/specification#request_object 48 */ 49 protected $nameRegex = '/^(?!^rpc\.)[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\.\\\]*$/'; 50 51 /** 52 * Parameter option types 53 * @var array 54 */ 55 protected $paramOptionTypes = array( 56 'name' => 'is_string', 57 'optional' => 'is_bool', 58 'default' => null, 59 'description' => 'is_string', 60 ); 61 62 /** 63 * Service params 64 * @var array 65 */ 66 protected $params = array(); 67 68 /** 69 * Mapping of parameter types to JSON-RPC types 70 * @var array 71 */ 72 protected $paramMap = array( 73 'any' => 'any', 74 'arr' => 'array', 75 'array' => 'array', 76 'assoc' => 'object', 77 'bool' => 'boolean', 78 'boolean' => 'boolean', 79 'dbl' => 'float', 80 'double' => 'float', 81 'false' => 'boolean', 82 'float' => 'float', 83 'hash' => 'object', 84 'integer' => 'integer', 85 'int' => 'integer', 86 'mixed' => 'any', 87 'nil' => 'null', 88 'null' => 'null', 89 'object' => 'object', 90 'string' => 'string', 91 'str' => 'string', 92 'struct' => 'object', 93 'true' => 'boolean', 94 'void' => 'null', 95 ); 96 97 /** 98 * Allowed transport types 99 * @var array 100 */ 101 protected $transportTypes = array( 102 'POST', 103 ); 104 105 /** 106 * Constructor 107 * 108 * @param string|array $spec 109 * @throws InvalidArgumentException if no name provided 110 */ 111 public function __construct($spec) 112 { 113 if (is_string($spec)) { 114 $this->setName($spec); 115 } elseif (is_array($spec)) { 116 $this->setOptions($spec); 117 } 118 119 if (null == $this->getName()) { 120 throw new InvalidArgumentException('SMD service description requires a name; none provided'); 121 } 122 } 123 124 /** 125 * Set object state 126 * 127 * @param array $options 128 * @return Service 129 */ 130 public function setOptions(array $options) 131 { 132 $methods = get_class_methods($this); 133 foreach ($options as $key => $value) { 134 if ('options' == strtolower($key)) { 135 continue; 136 } 137 $method = 'set' . ucfirst($key); 138 if (in_array($method, $methods)) { 139 $this->$method($value); 140 } 141 } 142 143 return $this; 144 } 145 146 /** 147 * Set service name 148 * 149 * @param string $name 150 * @return Service 151 * @throws InvalidArgumentException 152 */ 153 public function setName($name) 154 { 155 $name = (string) $name; 156 if (!preg_match($this->nameRegex, $name)) { 157 throw new InvalidArgumentException("Invalid name '{$name} provided for service; must follow PHP method naming conventions"); 158 } 159 $this->name = $name; 160 161 return $this; 162 } 163 164 /** 165 * Retrieve name 166 * 167 * @return string 168 */ 169 public function getName() 170 { 171 return $this->name; 172 } 173 174 /** 175 * Set Transport 176 * 177 * Currently limited to POST 178 * 179 * @param string $transport 180 * @throws InvalidArgumentException 181 * @return Service 182 */ 183 public function setTransport($transport) 184 { 185 if (!in_array($transport, $this->transportTypes)) { 186 throw new InvalidArgumentException("Invalid transport '{$transport}'; please select one of (" . implode(', ', $this->transportTypes) . ')'); 187 } 188 189 $this->transport = $transport; 190 191 return $this; 192 } 193 194 /** 195 * Get transport 196 * 197 * @return string 198 */ 199 public function getTransport() 200 { 201 return $this->transport; 202 } 203 204 /** 205 * Set service target 206 * 207 * @param string $target 208 * @return Service 209 */ 210 public function setTarget($target) 211 { 212 $this->target = (string) $target; 213 214 return $this; 215 } 216 217 /** 218 * Get service target 219 * 220 * @return string 221 */ 222 public function getTarget() 223 { 224 return $this->target; 225 } 226 227 /** 228 * Set envelope type 229 * 230 * @param string $envelopeType 231 * @throws InvalidArgumentException 232 * @return Service 233 */ 234 public function setEnvelope($envelopeType) 235 { 236 if (!in_array($envelopeType, $this->envelopeTypes)) { 237 throw new InvalidArgumentException("Invalid envelope type '{$envelopeType}'; please specify one of (" . implode(', ', $this->envelopeTypes) . ')'); 238 } 239 240 $this->envelope = $envelopeType; 241 242 return $this; 243 } 244 245 /** 246 * Get envelope type 247 * 248 * @return string 249 */ 250 public function getEnvelope() 251 { 252 return $this->envelope; 253 } 254 255 /** 256 * Add a parameter to the service 257 * 258 * @param string|array $type 259 * @param array $options 260 * @param int|null $order 261 * @throws InvalidArgumentException 262 * @return Service 263 */ 264 public function addParam($type, array $options = array(), $order = null) 265 { 266 if (is_string($type)) { 267 $type = $this->_validateParamType($type); 268 } elseif (is_array($type)) { 269 foreach ($type as $key => $paramType) { 270 $type[$key] = $this->_validateParamType($paramType); 271 } 272 } else { 273 throw new InvalidArgumentException('Invalid param type provided'); 274 } 275 276 $paramOptions = array( 277 'type' => $type, 278 ); 279 foreach ($options as $key => $value) { 280 if (in_array($key, array_keys($this->paramOptionTypes))) { 281 if (null !== ($callback = $this->paramOptionTypes[$key])) { 282 if (!$callback($value)) { 283 continue; 284 } 285 } 286 $paramOptions[$key] = $value; 287 } 288 } 289 290 $this->params[] = array( 291 'param' => $paramOptions, 292 'order' => $order, 293 ); 294 295 return $this; 296 } 297 298 /** 299 * Add params 300 * 301 * Each param should be an array, and should include the key 'type'. 302 * 303 * @param array $params 304 * @return Service 305 */ 306 public function addParams(array $params) 307 { 308 ksort($params); 309 foreach ($params as $options) { 310 if (!is_array($options)) { 311 continue; 312 } 313 if (!array_key_exists('type', $options)) { 314 continue; 315 } 316 $type = $options['type']; 317 $order = (array_key_exists('order', $options)) ? $options['order'] : null; 318 $this->addParam($type, $options, $order); 319 } 320 321 return $this; 322 } 323 324 /** 325 * Overwrite all parameters 326 * 327 * @param array $params 328 * @return Service 329 */ 330 public function setParams(array $params) 331 { 332 $this->params = array(); 333 334 return $this->addParams($params); 335 } 336 337 /** 338 * Get all parameters 339 * 340 * Returns all params in specified order. 341 * 342 * @return array 343 */ 344 public function getParams() 345 { 346 $params = array(); 347 $index = 0; 348 foreach ($this->params as $param) { 349 if (null === $param['order']) { 350 if (array_search($index, array_keys($params), true)) { 351 ++$index; 352 } 353 $params[$index] = $param['param']; 354 ++$index; 355 } else { 356 $params[$param['order']] = $param['param']; 357 } 358 } 359 ksort($params); 360 361 return $params; 362 } 363 364 /** 365 * Set return type 366 * 367 * @param string|array $type 368 * @throws InvalidArgumentException 369 * @return Service 370 */ 371 public function setReturn($type) 372 { 373 if (is_string($type)) { 374 $type = $this->_validateParamType($type, true); 375 } elseif (is_array($type)) { 376 foreach ($type as $key => $returnType) { 377 $type[$key] = $this->_validateParamType($returnType, true); 378 } 379 } else { 380 throw new InvalidArgumentException("Invalid param type provided ('" . gettype($type) . "')"); 381 } 382 $this->return = $type; 383 384 return $this; 385 } 386 387 /** 388 * Get return type 389 * 390 * @return string|array 391 */ 392 public function getReturn() 393 { 394 return $this->return; 395 } 396 397 /** 398 * Cast service description to array 399 * 400 * @return array 401 */ 402 public function toArray() 403 { 404 $envelope = $this->getEnvelope(); 405 $target = $this->getTarget(); 406 $transport = $this->getTransport(); 407 $parameters = $this->getParams(); 408 $returns = $this->getReturn(); 409 $name = $this->getName(); 410 411 if (empty($target)) { 412 return compact('envelope', 'transport', 'name', 'parameters', 'returns'); 413 } 414 415 return compact('envelope', 'target', 'transport', 'name', 'parameters', 'returns'); 416 } 417 418 /** 419 * Return JSON encoding of service 420 * 421 * @return string 422 */ 423 public function toJson() 424 { 425 $service = array($this->getName() => $this->toArray()); 426 427 return \Zend\Json\Json::encode($service); 428 } 429 430 /** 431 * Cast to string 432 * 433 * @return string 434 */ 435 public function __toString() 436 { 437 return $this->toJson(); 438 } 439 440 /** 441 * Validate parameter type 442 * 443 * @param string $type 444 * @param bool $isReturn 445 * @return string 446 * @throws InvalidArgumentException 447 */ 448 protected function _validateParamType($type, $isReturn = false) 449 { 450 if (!is_string($type)) { 451 throw new InvalidArgumentException("Invalid param type provided ('{$type}')"); 452 } 453 454 if (!array_key_exists($type, $this->paramMap)) { 455 $type = 'object'; 456 } 457 458 $paramType = $this->paramMap[$type]; 459 if (!$isReturn && ('null' == $paramType)) { 460 throw new InvalidArgumentException("Invalid param type provided ('{$type}')"); 461 } 462 463 return $paramType; 464 } 465} 466