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