Completed
Push — master ( c93e6b...952773 )
by Evgeny
03:07
created

Service::parent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright 2016, Cake Development Corporation (http://cakedc.com)
4
 *
5
 * Licensed under The MIT License
6
 * Redistributions of files must retain the above copyright notice.
7
 *
8
 * @copyright Copyright 2016, Cake Development Corporation (http://cakedc.com)
9
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
10
 */
11
12
namespace CakeDC\Api\Service;
13
14
use CakeDC\Api\Routing\ApiRouter;
15
use CakeDC\Api\Service\Action\DummyAction;
16
use CakeDC\Api\Service\Action\Result;
17
use CakeDC\Api\Service\Exception\MissingActionException;
18
use CakeDC\Api\Service\Exception\MissingParserException;
19
use CakeDC\Api\Service\Exception\MissingRendererException;
20
use CakeDC\Api\Service\Renderer\BaseRenderer;
21
use CakeDC\Api\Service\RequestParser\BaseParser;
22
use Cake\Core\App;
23
use Cake\Core\Configure;
24
use Cake\Datasource\Exception\RecordNotFoundException;
25
use Cake\Http\Client\Response;
26
use Cake\Routing\RouteBuilder;
27
use Cake\Utility\Hash;
28
use Cake\Utility\Inflector;
29
use Exception;
30
31
/**
32
 * Class Service
33
 */
34
abstract class Service
35
{
36
37
    /**
38
     * Actions routes description map, indexed by action name.
39
     *
40
     * @var array
41
     */
42
    protected $_actions = [];
43
44
    /**
45
     * Actions classes map, indexed by action name.
46
     *
47
     * @var array
48
     */
49
    protected $_actionsClassMap = [];
50
51
    /**
52
     * Service url acceptable extensions list.
53
     *
54
     * @var array
55
     */
56
    protected $_extensions = ['json'];
57
58
    /**
59
     *
60
     *
61
     * @var string
62
     */
63
    protected $_routePrefix = '';
64
65
    /**
66
     * Service name
67
     *
68
     * @var string
69
     */
70
    protected $_name = null;
71
72
    /**
73
     * Service version.
74
     *
75
     * @var int
76
     */
77
    protected $_version;
78
79
80
    /**
81
     * Parser class to process the HTTP request.
82
     *
83
     * @var BaseParser
84
     */
85
    protected $_parser;
86
87
    /**
88
     * Renderer class to build the HTTP response.
89
     *
90
     * @var BaseRenderer
91
     */
92
    protected $_renderer;
93
94
    /**
95
     * The parser class.
96
     *
97
     * @var string
98
     */
99
    protected $_parserClass = null;
100
101
    /**
102
     * The Renderer class.
103
     *
104
     * @var string
105
     */
106
    protected $_rendererClass = null;
107
108
    /**
109
     * Dependent services names list
110
     *
111
     * @var array<string>
112
     */
113
    protected $_innerServices = [];
114
115
    /**
116
     * Parent service instance.
117
     *
118
     * @var Service
119
     */
120
    protected $_parentService;
121
122
    /**
123
     * Service Action Result object.
124
     *
125
     * @var Result
126
     */
127
    protected $_result;
128
129
    /**
130
     * Base url for service.
131
     *
132
     * @var string
133
     */
134
    protected $_baseUrl;
135
136
    /**
137
     * Request
138
     *
139
     * @var \Cake\Network\Request
140
     */
141
    protected $_request;
142
143
    /**
144
     * Request
145
     *
146
     * @var \Cake\Network\Response
147
     */
148
    protected $_response;
149
150
    /**
151
     * @var string
152
     */
153
    protected $_corsSuffix = '_cors';
154
155
    /**
156
     * Service constructor.
157
     *
158
     * @param array $config Service configuration.
159
     */
160 118
    public function __construct(array $config = [])
161
    {
162 118
        if (isset($config['request'])) {
163 118
            $this->request($config['request']);
164 118
        }
165 118
        if (isset($config['response'])) {
166 118
            $this->response($config['response']);
167 118
        }
168 118
        if (isset($config['baseUrl'])) {
169 78
            $this->_baseUrl = $config['baseUrl'];
170 78
        }
171 118
        if (isset($config['service'])) {
172 96
            $this->name($config['service']);
173 96
        }
174 118
        if (isset($config['version'])) {
175
            $this->version($config['version']);
176
        }
177 118
        if (isset($config['classMap'])) {
178 1
            $this->_actionsClassMap = Hash::merge($this->_actionsClassMap, $config['classMap']);
179 1
        }
180 118
        $this->initialize();
181 118
        $this->_initializeParser($config);
182 118
        $this->_initializeRenderer($config);
183 118
    }
184
185
    /**
186
     * Get and set service name.
187
     *
188
     * @param string $name Service name.
189
     * @return string
190
     */
191 118
    public function name($name = null)
192
    {
193 118
        if ($name === null) {
194 117
            return $this->_name;
195
        }
196 118
        $this->_name = $name;
197
198 118
        return $this->_name;
199
    }
200
201
    /**
202
     * Get and set service version.
203
     *
204
     * @param int $version Version number.
205
     * @return int
206
     */
207 80
    public function version($version = null)
208
    {
209 80
        if ($version === null) {
210 80
            return $this->_version;
211
        }
212
        $this->_version = $version;
213
214
        return $this->_version;
215
    }
216
217
    /**
218
     * Initialize method
219
     *
220
     * @return void
221
     */
222 118
    public function initialize()
223
    {
224 118
        if ($this->_name === null) {
225 39
            $className = (new \ReflectionClass($this))->getShortName();
226 39
            $this->name(Inflector::underscore(str_replace('Service', '', $className)));
0 ignored issues
show
Bug introduced by
It seems like \Cake\Utility\Inflector:...vice', '', $className)) targeting Cake\Utility\Inflector::underscore() can also be of type boolean; however, CakeDC\Api\Service\Service::name() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
227 39
        }
228 118
    }
229
230
    /**
231
     * Service parser configuration method.
232
     *
233
     * @param BaseParser $parser A Parser instance.
234
     * @return BaseParser
235
     */
236 51
    public function parser(BaseParser $parser = null)
237
    {
238 51
        if ($parser === null) {
239 51
            return $this->_parser;
240
        }
241
        $this->_parser = $parser;
242
243
        return $this->_parser;
244
    }
245
246
    /**
247
     * Get and set request.
248
     *
249
     * @param \Cake\Network\Request $request A request object.
250
     * @return \Cake\Network\Request
251
     */
252 118
    public function request($request = null)
253
    {
254 118
        if ($request === null) {
255 101
            return $this->_request;
256
        }
257
258 118
        $this->_request = $request;
259
260 118
        return $this->_request;
261
    }
262
263
    /**
264
     * Get the service route scopes and their connected routes.
265
     *
266
     * @return array
267
     */
268 3
    public function routes()
269
    {
270
        return $this->_routesWrapper(function () {
271 3
            return ApiRouter::routes();
272 3
        });
273
    }
274
275
    /**
276
     * @param callable $callable Wrapped router instance.
277
     * @return mixed
278
     */
279 64
    protected function _routesWrapper(callable $callable)
280
    {
281 64
        $this->resetRoutes();
282 64
        $this->loadRoutes();
283 64
        ApiRouter::$initialized = true;
284 64
        $result = $callable();
285 63
        $this->resetRoutes();
286
287 63
        return $result;
288
    }
289
290
    /**
291
     * Reset to default application routes.
292
     *
293
     * @return void
294
     */
295 64
    public function resetRoutes()
296
    {
297 64
        ApiRouter::reload();
298 64
    }
299
300
    /**
301
     * Initialize service level routes
302
     *
303
     * @return void
304
     */
305 8
    public function loadRoutes()
306
    {
307 8
        $defaultOptions = $this->routerDefaultOptions();
308
        ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
309 8
            if (is_array($this->_extensions)) {
310 8
                $routes->extensions($this->_extensions);
311 8
            }
312 8
            if (!empty($defaultOptions['map'])) {
313 8
                $routes->resources($this->name(), $defaultOptions);
314 8
            }
315 8
        });
316 8
    }
317
318
    /**
319
     * Build router settings.
320
     * This implementation build action map for resource routes based on Service actions.
321
     *
322
     * @return array
323
     */
324 63
    public function routerDefaultOptions()
325
    {
326 63
        $mapList = [];
327 63
        foreach ($this->_actions as $alias => $map) {
328 42
            if (is_numeric($alias)) {
329
                $alias = $map;
330
                $map = [];
331
            }
332 42
            $mapCors = false;
333 42
            if (!empty($map['mapCors'])) {
334 8
                $mapCors = $map['mapCors'];
335 8
                unset($map['mapCors']);
336 8
            }
337 42
            $mapList[$alias] = $map;
338 42
            $mapList[$alias] += ['method' => 'GET', 'path' => '', 'action' => $alias];
339 42
            if ($mapCors) {
340 8
                $map['method'] = 'OPTIONS';
341 8
                $map += ['path' => '', 'action' => $alias . $this->_corsSuffix];
342 8
                $mapList[$alias . $this->_corsSuffix] = $map;
343 8
            }
344 63
        }
345
346
        return [
347
            'map' => $mapList
348 63
        ];
349
    }
350
351
    /**
352
     * Finds URL for specified action.
353
     *
354
     * Returns an URL pointing to a combination of controller and action.
355
     *
356
     * @param string|array|null $route An array specifying any of the following:
357
     *   'controller', 'action', 'plugin' additionally, you can provide routed
358
     *   elements or query string parameters. If string it can be name any valid url
359
     *   string.
360
     * @return string Full translated URL with base path.
361
     * @throws \Cake\Core\Exception\Exception When the route name is not found
362
     */
363
    public function routeUrl($route)
364
    {
365
        return $this->_routesWrapper(function () use ($route) {
366
            return ApiRouter::url($route);
367
        });
368
    }
369
370
    /**
371
     * Reverses a parsed parameter array into a string.
372
     *
373
     * @param \Cake\Network\Request|array $params The params array or
374
     *     Cake\Network\Request object that needs to be reversed.
375
     * @return string The string that is the reversed result of the array
376
     */
377 13
    public function routeReverse($params)
378
    {
379
        return $this->_routesWrapper(function () use ($params) {
380
            try {
381 13
                return ApiRouter::reverse($params);
382 1
            } catch (Exception $e) {
383 1
                return null;
384
            }
385 13
        });
386
    }
387
388
    /**
389
     * Dispatch service call.
390
     *
391
     * @return \CakeDC\Api\Service\Action\Result
392
     */
393 56
    public function dispatch()
394
    {
395
        try {
396 56
            $action = $this->buildAction();
397 56
            $result = $action->process();
398
399 52
            if ($result instanceof Result) {
400
                $this->result($result);
401
            }  else {
402 52
                $this->result()->data($result);
403 52
                $this->result()->code(200);
404
            }
405 56
        } catch (RecordNotFoundException $e) {
406 6
            $this->result()->code(404);
407 6
            $this->result()->exception($e);
408 8
        } catch (Exception $e) {
409 2
            $code = $e->getCode();
410 2
            if (!is_int($code) || $code < 100 || $code >= 600) {
411
                $this->result()->code(500);
412
            }
413 2
            $this->result()->exception($e);
414
        }
415
416 56
        return $this->result();
417
    }
418
419
    /**
420
     * Build action instance
421
     *
422
     * @return \CakeDC\Api\Service\Action\Action
423
     * @throws Exception
424
     */
425 63
    public function buildAction()
426
    {
427 63
        $route = $this->parseRoute($this->baseUrl());
428 62
        if (empty($route)) {
429
            throw new MissingActionException('Invalid Action Route:' . $this->baseUrl()); // InvalidActionException
430
        }
431 62
        $service = null;
432 62
        $serviceName = Inflector::underscore($route['controller']);
433 62
        if ($serviceName == $this->name()) {
434 53
            $service = $this;
435 53
        }
436 62
        if (in_array($serviceName, $this->_innerServices)) {
437
            $options = [
438
//                'service' => $this->name(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
439 9
                'version' => $this->version(),
440 9
                'request' => $this->request(),
441 9
                'response' => $this->response(),
442
//                'baseUrl' => $this->baseUrl(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
443 9
                'refresh' => true,
444 9
            ];
445 9
            $service = ServiceRegistry::get($serviceName, $options);
0 ignored issues
show
Bug introduced by
It seems like $serviceName defined by \Cake\Utility\Inflector:...e($route['controller']) on line 432 can also be of type boolean; however, CakeDC\Api\Service\ServiceRegistry::get() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
446 9
            $service->parent($this);
447 9
        }
448 62
        $action = $route['action'];
449 62
        list($namespace, $serviceClass) = namespaceSplit(get_class($service));
450 62
        $actionPrefix = substr($serviceClass, 0, -7);
451 62
        $actionClass = $namespace . '\\Action\\' . $actionPrefix . Inflector::camelize($action) . 'Action';
452 62
        if (class_exists($actionClass)) {
453 2
            return $service->buildActionClass($actionClass, $route);
454
        }
455 60
        if (array_key_exists($action, $this->_actionsClassMap)) {
456 60
            $actionClass = $this->_actionsClassMap[$action];
457
458 60
            return $service->buildActionClass($actionClass, $route);
459
        }
460
        throw new MissingActionException(['class' => $actionClass]);
461
    }
462
463
    /**
464
     * Parses given URL string. Returns 'routing' parameters for that URL.
465
     *
466
     * @param string $url URL to be parsed
467
     * @return array Parsed elements from URL
468
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
469
     */
470
    public function parseRoute($url)
471
    {
472 63
        return $this->_routesWrapper(function () use ($url) {
473 63
            return ApiRouter::parse($url);
474 63
        });
475
    }
476
477
    /**
478
     * Build base url
479
     *
480
     * @return string
481
     */
482 64
    public function baseUrl()
483
    {
484 64
        if (!empty($this->_baseUrl)) {
485 64
            return $this->_baseUrl;
486
        }
487
488
        $result = '/' . $this->name();
489
490
        return $result;
491
    }
492
493
    /**
494
     * Parent service get and set methods
495
     *
496
     * @param Service $service Parent Service instance.
497
     * @return Service
498
     */
499 54
    public function parent(Service $service = null)
500
    {
501 54
        if ($service === null) {
502 54
            return $this->_parentService;
503
        }
504 9
        $this->_parentService = $service;
505
506 9
        return $this->_parentService;
507
    }
508
509
    /**
510
     * Build action class
511
     *
512
     * @param string $class Class name.
513
     * @param array $route Activated route.
514
     * @return mixed
515
     */
516 63
    public function buildActionClass($class, $route)
517
    {
518 63
        $instance = new $class($this->_actionOptions($route));
519
520 63
        return $instance;
521
    }
522
523
    /**
524
     * Action constructor options.
525
     *
526
     * @param array $route Activated route.
527
     * @return array
528
     */
529 63
    protected function _actionOptions($route)
530
    {
531 63
        $actionName = $route['action'];
532
533
        $options = [
534 63
            'name' => $actionName,
535 63
            'service' => $this,
536 63
            'route' => $route,
537 63
        ];
538 63
        $options += (new ConfigReader())->actionOptions($this->name(), $actionName, $this->version());
539
540 63
        return $options;
541
    }
542
543
    /**
544
     * @return \CakeDC\Api\Service\Action\Result
545
     */
546 56
    public function result($value = null)
547
    {
548 56
        if ($this->_parentService !== null) {
549 2
            return $this->_parentService->result($value);
550
        }
551 56
        if ($value instanceof Result) {
552
            $this->_result = $value;
553
        }
554 56
        if ($this->_result === null) {
555 56
            $this->_result = new Result();
556 56
        }
557
558 56
        return $this->_result;
559
    }
560
561
    /**
562
     * Fill up response and stop execution.
563
     *
564
     * @param Result $result A Result instance.
565
     * @return Response
566
     */
567 56
    public function respond($result = null)
568
    {
569 56
        if ($result === null) {
570
            $result = $this->result();
571
        }
572 56
        $this->response()
573 56
             ->statusCode($result->code());
574 56
        if ($result->exception() !== null) {
575 8
            $this->renderer()
576 8
                 ->error($result->exception());
577 8
        } else {
578 52
            $this->renderer()
579 52
                 ->response($result);
580
        }
581
582 56
        return $this->response();
583
    }
584
585
    /**
586
     * Get and set response.
587
     *
588
     * @param \Cake\Network\Response $response  A Response object.
589
     * @return \Cake\Network\Response
590
     */
591 118
    public function response($response = null)
592
    {
593 118
        if ($response === null) {
594 109
            return $this->_response;
595
        }
596
597 118
        $this->_response = $response;
598
599 118
        return $this->_response;
600
    }
601
602
    /**
603
     * Service renderer configuration method.
604
     *
605
     * @param BaseRenderer $renderer A Renderer instance.
606
     * @return BaseRenderer
607
     */
608 68
    public function renderer(BaseRenderer $renderer = null)
609
    {
610 68
        if ($renderer === null) {
611 68
            return $this->_renderer;
612
        }
613
        $this->_renderer = $renderer;
614
615
        return $this->_renderer;
616
    }
617
618
    /**
619
     * Define action config.
620
     *
621
     * @param string $actionName Action name.
622
     * @param string $className Class name.
623
     * @param array $route Route config.
624
     * @return void
625
     */
626 8
    public function mapAction($actionName, $className, $route)
627
    {
628 8
        $route += ['mapCors' => false];
629 8
        $this->_actionsClassMap[$actionName] = $className;
630 8
        if ($route['mapCors']) {
631 8
            $this->_actionsClassMap[$actionName . $this->_corsSuffix] = DummyAction::class;
632 8
        }
633 8
        if (!isset($route['path'])) {
634 8
            $route['path'] = $actionName;
635 8
        }
636 8
        $this->_actions[$actionName] = $route;
637 8
    }
638
639
    /**
640
     * Initialize parser.
641
     *
642
     * @param array $config Service options
643
     * @return void
644
     */
645 118
    protected function _initializeParser(array $config)
646
    {
647 118
        if (empty($this->_parserClass) && isset($config['parserClass'])) {
648
            $this->_parserClass = $config['parserClass'];
649
        }
650 118
        $parserClass = Configure::read('Api.parser');
651 118
        if (empty($this->_parserClass) && !empty($parserClass)) {
652 118
            $this->_parserClass = $parserClass;
653 118
        }
654
655 118
        $class = App::className($this->_parserClass, 'Service/RequestParser', 'Parser');
656 118
        if (!class_exists($class)) {
657
            throw new MissingParserException(['class' => $this->_parserClass]);
658
        }
659 118
        $this->_parser = new $class($this);
660 118
    }
661
662
    /**
663
     * Initialize renderer.
664
     *
665
     * @param array $config Service options.
666
     * @return void
667
     */
668 118
    protected function _initializeRenderer(array $config)
669
    {
670 118
        if (empty($this->_rendererClass) && isset($config['rendererClass'])) {
671 13
            $this->_rendererClass = $config['rendererClass'];
672 13
        }
673 118
        $rendererClass = Configure::read('Api.renderer');
674 118
        if (empty($this->_rendererClass) && !empty($rendererClass)) {
675 105
            $this->_rendererClass = $rendererClass;
676 105
        }
677
678 118
        $class = App::className($this->_rendererClass, 'Service/Renderer', 'Renderer');
679 118
        if (!class_exists($class)) {
680
            throw new MissingRendererException(['class' => $this->_rendererClass]);
681
        }
682 118
        $this->_renderer = new $class($this);
683 118
    }
684
}
685