Completed
Push — master ( 952773...a13902 )
by Evgeny
02:39
created

Service::__construct()   F

Complexity

Conditions 10
Paths 512

Size

Total Lines 38
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.1953

Importance

Changes 0
Metric Value
cc 10
eloc 25
c 0
b 0
f 0
nc 512
nop 1
dl 0
loc 38
rs 3.2187
ccs 14
cts 16
cp 0.875
crap 10.1953

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Cake\Event\EventDispatcherInterface;
15
use Cake\Event\EventDispatcherTrait;
16
use Cake\Event\EventListenerInterface;
17
use Cake\Event\EventManager;
18
use CakeDC\Api\Routing\ApiRouter;
19
use CakeDC\Api\Service\Action\DummyAction;
20
use CakeDC\Api\Service\Action\Result;
21
use CakeDC\Api\Service\Exception\MissingActionException;
22
use CakeDC\Api\Service\Exception\MissingParserException;
23
use CakeDC\Api\Service\Exception\MissingRendererException;
24
use CakeDC\Api\Service\Renderer\BaseRenderer;
25
use CakeDC\Api\Service\RequestParser\BaseParser;
26
use Cake\Core\App;
27
use Cake\Core\Configure;
28
use Cake\Datasource\Exception\RecordNotFoundException;
29
use Cake\Http\Client\Response;
30
use Cake\Routing\RouteBuilder;
31
use Cake\Utility\Hash;
32
use Cake\Utility\Inflector;
33
use Exception;
34
35
/**
36
 * Class Service
37
 */
38
abstract class Service implements EventListenerInterface, EventDispatcherInterface
39
{
40
    use EventDispatcherTrait;
41
42
    /**
43
     * Extensions to load and attach to listener
44
     *
45
     * @var array
46
     */
47
    public $extensions = [];
48
49
    /**
50
     * Actions routes description map, indexed by action name.
51
     *
52
     * @var array
53
     */
54
    protected $_actions = [];
55
56
    /**
57
     * Actions classes map, indexed by action name.
58
     *
59
     * @var array
60
     */
61
    protected $_actionsClassMap = [];
62
63
    /**
64
     * Service url acceptable extensions list.
65
     *
66
     * @var array
67
     */
68
    protected $_routeExtensions = ['json'];
69
70
    /**
71
     *
72
     *
73
     * @var string
74
     */
75
    protected $_routePrefix = '';
76
77
    /**
78
     * Service name
79
     *
80
     * @var string
81
     */
82
    protected $_name = null;
83
84
    /**
85
     * Service version.
86
     *
87
     * @var int
88
     */
89
    protected $_version;
90
91
    /**
92
     * Parser class to process the HTTP request.
93
     *
94
     * @var BaseParser
95
     */
96
    protected $_parser;
97
98
    /**
99
     * Renderer class to build the HTTP response.
100
     *
101
     * @var BaseRenderer
102
     */
103
    protected $_renderer;
104
105
    /**
106
     * The parser class.
107
     *
108
     * @var string
109
     */
110
    protected $_parserClass = null;
111
112
    /**
113
     * The Renderer class.
114
     *
115
     * @var string
116
     */
117
    protected $_rendererClass = null;
118
119
    /**
120
     * Dependent services names list
121
     *
122
     * @var array<string>
123
     */
124
    protected $_innerServices = [];
125
126
    /**
127
     * Parent service instance.
128
     *
129
     * @var Service
130
     */
131
    protected $_parentService;
132
133
    /**
134
     * Service Action Result object.
135
     *
136
     * @var Result
137
     */
138
    protected $_result;
139
140
    /**
141
     * Base url for service.
142
     *
143
     * @var string
144
     */
145
    protected $_baseUrl;
146
147
    /**
148
     * Request
149
     *
150
     * @var \Cake\Network\Request
151
     */
152
    protected $_request;
153
154
    /**
155
     * Request
156
     *
157
     * @var \Cake\Network\Response
158
     */
159
    protected $_response;
160 118
161
    /**
162 118
     * @var string
163 118
     */
164 118
    protected $_corsSuffix = '_cors';
165 118
166 118
    /**
167 118
     * Extension registry.
168 118
     *
169 78
     * @var \CakeDC\Api\Service\ExtensionRegistry
170 78
     */
171 118
    protected $_extensions;
172 96
173 96
    /**
174 118
     * Service constructor.
175
     *
176
     * @param array $config Service configuration.
177 118
     */
178 1
    public function __construct(array $config = [])
179 1
    {
180 118
        if (isset($config['request'])) {
181 118
            $this->request($config['request']);
182 118
        }
183 118
        if (isset($config['response'])) {
184
            $this->response($config['response']);
185
        }
186
        if (isset($config['baseUrl'])) {
187
            $this->_baseUrl = $config['baseUrl'];
188
        }
189
        if (isset($config['service'])) {
190
            $this->name($config['service']);
191 118
        }
192
        if (isset($config['version'])) {
193 118
            $this->version($config['version']);
194 117
        }
195
        if (isset($config['classMap'])) {
196 118
            $this->_actionsClassMap = Hash::merge($this->_actionsClassMap, $config['classMap']);
197
        }
198 118
		
199
        if (!empty($config['Extension'])) {
200
            $this->extensions = (Hash::merge($this->extensions, $config['Extension']));
201
        }
202
        $extensionRegistry = $eventManager = null;
203
        if (!empty($config['eventManager'])) {
204
            $eventManager = $config['eventManager'];
205
        }
206
        $this->_eventManager = $eventManager ?: new EventManager();
207 80
208
        $this->initialize();
209 80
        $this->_initializeParser($config);
210 80
        $this->_initializeRenderer($config);
211
        $this->_eventManager->on($this);
212
        $this->extensions($extensionRegistry);
213
        $this->_loadExtensions();
214
		
215
    }
216
217
    /**
218
     * Get and set service name.
219
     *
220
     * @param string $name Service name.
221
     * @return string
222 118
     */
223
    public function name($name = null)
224 118
    {
225 39
        if ($name === null) {
226 39
            return $this->_name;
227 39
        }
228 118
        $this->_name = $name;
229
230
        return $this->_name;
231
    }
232
233
    /**
234
     * Get and set service version.
235
     *
236 51
     * @param int $version Version number.
237
     * @return int
238 51
     */
239 51
    public function version($version = null)
240
    {
241
        if ($version === null) {
242
            return $this->_version;
243
        }
244
        $this->_version = $version;
245
246
        return $this->_version;
247
    }
248
249
    /**
250
     * Initialize method
251
     *
252 118
     * @return void
253
     */
254 118
    public function initialize()
255 101
    {
256
        if ($this->_name === null) {
257
            $className = (new \ReflectionClass($this))->getShortName();
258 118
            $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...
259
        }
260 118
    }
261
262
    /**
263
     * Service parser configuration method.
264
     *
265
     * @param BaseParser $parser A Parser instance.
266
     * @return BaseParser
267
     */
268 3
    public function parser(BaseParser $parser = null)
269
    {
270
        if ($parser === null) {
271 3
            return $this->_parser;
272 3
        }
273
        $this->_parser = $parser;
274
275
        return $this->_parser;
276
    }
277
278
    /**
279 64
     * Get and set request.
280
     *
281 64
     * @param \Cake\Network\Request $request A request object.
282 64
     * @return \Cake\Network\Request
283 64
     */
284 64
    public function request($request = null)
285 63
    {
286
        if ($request === null) {
287 63
            return $this->_request;
288
        }
289
290
        $this->_request = $request;
291
292
        return $this->_request;
293
    }
294
295 64
    /**
296
     * Get the service route scopes and their connected routes.
297 64
     *
298 64
     * @return array
299
     */
300
    public function routes()
301
    {
302
        return $this->_routesWrapper(function () {
303
            return ApiRouter::routes();
304
        });
305 8
    }
306
307 8
    /**
308
     * @param callable $callable Wrapped router instance.
309 8
     * @return mixed
310 8
     */
311 8
    protected function _routesWrapper(callable $callable)
312 8
    {
313 8
        $this->resetRoutes();
314 8
        $this->loadRoutes();
315 8
        ApiRouter::$initialized = true;
316 8
        $result = $callable();
317
        $this->resetRoutes();
318
319
        return $result;
320
    }
321
322
    /**
323
     * Reset to default application routes.
324 63
     *
325
     * @return void
326 63
     */
327 63
    public function resetRoutes()
328 42
    {
329
        ApiRouter::reload();
330
    }
331
332 42
    /**
333 42
     * Initialize service level routes
334 8
     *
335 8
     * @return void
336 8
     */
337 42
    public function loadRoutes()
338 42
    {
339 42
        $defaultOptions = $this->routerDefaultOptions();
340 8
        ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
341 8
            if (is_array($this->_routeExtensions)) {
342 8
                $routes->extensions($this->_routeExtensions);
343 8
            }
344 63
            if (!empty($defaultOptions['map'])) {
345
                $routes->resources($this->name(), $defaultOptions);
346
            }
347
        });
348 63
    }
349
350
    /**
351
     * Build router settings.
352
     * This implementation build action map for resource routes based on Service actions.
353
     *
354
     * @return array
355
     */
356
    public function routerDefaultOptions()
357
    {
358
        $mapList = [];
359
        foreach ($this->_actions as $alias => $map) {
360
            if (is_numeric($alias)) {
361
                $alias = $map;
362
                $map = [];
363
            }
364
            $mapCors = false;
365
            if (!empty($map['mapCors'])) {
366
                $mapCors = $map['mapCors'];
367
                unset($map['mapCors']);
368
            }
369
            $mapList[$alias] = $map;
370
            $mapList[$alias] += ['method' => 'GET', 'path' => '', 'action' => $alias];
371
            if ($mapCors) {
372
                $map['method'] = 'OPTIONS';
373
                $map += ['path' => '', 'action' => $alias . $this->_corsSuffix];
374
                $mapList[$alias . $this->_corsSuffix] = $map;
375
            }
376
        }
377 13
378
        return [
379
            'map' => $mapList
380
        ];
381 13
    }
382 1
383 1
    /**
384
     * Finds URL for specified action.
385 13
     *
386
     * Returns an URL pointing to a combination of controller and action.
387
     *
388
     * @param string|array|null $route An array specifying any of the following:
389
     *   'controller', 'action', 'plugin' additionally, you can provide routed
390
     *   elements or query string parameters. If string it can be name any valid url
391
     *   string.
392
     * @return string Full translated URL with base path.
393 56
     * @throws \Cake\Core\Exception\Exception When the route name is not found
394
     */
395
    public function routeUrl($route)
396 56
    {
397 56
        return $this->_routesWrapper(function () use ($route) {
398
            return ApiRouter::url($route);
399 52
        });
400
    }
401
402 52
    /**
403 52
     * Reverses a parsed parameter array into a string.
404
     *
405 56
     * @param \Cake\Network\Request|array $params The params array or
406 6
     *     Cake\Network\Request object that needs to be reversed.
407 6
     * @return string The string that is the reversed result of the array
408 8
     */
409 2
    public function routeReverse($params)
410 2
    {
411
        return $this->_routesWrapper(function () use ($params) {
412
            try {
413 2
                return ApiRouter::reverse($params);
414
            } catch (Exception $e) {
415
                return null;
416 56
            }
417
        });
418
    }
419
420
    /**
421
     * Dispatch service call.
422
     *
423
     * @return \CakeDC\Api\Service\Action\Result
424
     */
425 63
    public function dispatch()
426
    {
427 63
        try {
428 62
			$this->dispatchEvent('Service.beforeDispatch', ['service' => $this]);
429
            $action = $this->buildAction();
430
			$this->dispatchEvent('Service.beforeProcess', ['service' => $this, 'action' => $this]);
431 62
            $result = $action->process();
432 62
433 62
            if ($result instanceof Result) {
434 53
                $this->result($result);
435 53
            }  else {
436 62
                $this->result()->data($result);
437
                $this->result()->code(200);
438
            }
439 9
        } catch (RecordNotFoundException $e) {
440 9
            $this->result()->code(404);
441 9
            $this->result()->exception($e);
442
        } catch (Exception $e) {
443 9
            $code = $e->getCode();
444 9
            if (!is_int($code) || $code < 100 || $code >= 600) {
445 9
                $this->result()->code(500);
446 9
            }
447 9
            $this->result()->exception($e);
448 62
        }
449 62
		$this->dispatchEvent('Service.afterDispatch', ['service' => $this]);
450 62
451 62
        return $this->result();
452 62
    }
453 2
454
    /**
455 60
     * Build action instance
456 60
     *
457
     * @return \CakeDC\Api\Service\Action\Action
458 60
     * @throws Exception
459
     */
460
    public function buildAction()
461
    {
462
        $route = $this->parseRoute($this->baseUrl());
463
        if (empty($route)) {
464
            throw new MissingActionException('Invalid Action Route:' . $this->baseUrl()); // InvalidActionException
465
        }
466
        $service = null;
467
        $serviceName = Inflector::underscore($route['controller']);
468
        if ($serviceName == $this->name()) {
469
            $service = $this;
470
        }
471
        if (in_array($serviceName, $this->_innerServices)) {
472 63
            $options = [
473 63
                'version' => $this->version(),
474 63
                'request' => $this->request(),
475
                'response' => $this->response(),
476
                'refresh' => true,
477
            ];
478
            $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 467 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...
479
            $service->parent($this);
480
        }
481
        $action = $route['action'];
482 64
        list($namespace, $serviceClass) = namespaceSplit(get_class($service));
483
        $actionPrefix = substr($serviceClass, 0, -7);
484 64
        $actionClass = $namespace . '\\Action\\' . $actionPrefix . Inflector::camelize($action) . 'Action';
485 64
        if (class_exists($actionClass)) {
486
            return $service->buildActionClass($actionClass, $route);
487
        }
488
        if (array_key_exists($action, $this->_actionsClassMap)) {
489
            $actionClass = $this->_actionsClassMap[$action];
490
491
            return $service->buildActionClass($actionClass, $route);
492
        }
493
        throw new MissingActionException(['class' => $actionClass]);
494
    }
495
496
    /**
497
     * Parses given URL string. Returns 'routing' parameters for that URL.
498
     *
499 54
     * @param string $url URL to be parsed
500
     * @return array Parsed elements from URL
501 54
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
502 54
     */
503
    public function parseRoute($url)
504 9
    {
505
        return $this->_routesWrapper(function () use ($url) {
506 9
            return ApiRouter::parse($url);
507
        });
508
    }
509
510
    /**
511
     * Build base url
512
     *
513
     * @return string
514
     */
515
    public function baseUrl()
516 63
    {
517
        if (!empty($this->_baseUrl)) {
518 63
            return $this->_baseUrl;
519
        }
520 63
521
        $result = '/' . $this->name();
522
523
        return $result;
524
    }
525
526
    /**
527
     * Parent service get and set methods
528
     *
529 63
     * @param Service $service Parent Service instance.
530
     * @return Service
531 63
     */
532
    public function parent(Service $service = null)
533
    {
534 63
        if ($service === null) {
535 63
            return $this->_parentService;
536 63
        }
537 63
        $this->_parentService = $service;
538 63
539
        return $this->_parentService;
540 63
    }
541
542
    /**
543
     * Build action class
544
     *
545
     * @param string $class Class name.
546 56
     * @param array $route Activated route.
547
     * @return mixed
548 56
     */
549 2
    public function buildActionClass($class, $route)
550
    {
551 56
        $instance = new $class($this->_actionOptions($route));
552
553
        return $instance;
554 56
    }
555 56
556 56
    /**
557
     * Action constructor options.
558 56
     *
559
     * @param array $route Activated route.
560
     * @return array
561
     */
562
    protected function _actionOptions($route)
563
    {
564
        $actionName = $route['action'];
565
566
        $options = [
567 56
            'name' => $actionName,
568
            'service' => $this,
569 56
            'route' => $route,
570
        ];
571
        $options += (new ConfigReader())->actionOptions($this->name(), $actionName, $this->version());
572 56
573 56
        return $options;
574 56
    }
575 8
576 8
    /**
577 8
     * @return \CakeDC\Api\Service\Action\Result
578 52
     */
579 52
    public function result($value = null)
580
    {
581
        if ($this->_parentService !== null) {
582 56
            return $this->_parentService->result($value);
583
        }
584
        if ($value instanceof Result) {
585
            $this->_result = $value;
586
        }
587
        if ($this->_result === null) {
588
            $this->_result = new Result();
589
        }
590
591 118
        return $this->_result;
592
    }
593 118
594 109
    /**
595
     * Fill up response and stop execution.
596
     *
597 118
     * @param Result $result A Result instance.
598
     * @return Response
599 118
     */
600
    public function respond($result = null)
601
    {
602
        if ($result === null) {
603
            $result = $this->result();
604
        }
605
        $this->response()
606
             ->statusCode($result->code());
607
        if ($result->exception() !== null) {
608 68
            $this->renderer()
609
                 ->error($result->exception());
610 68
        } else {
611 68
            $this->renderer()
612
                 ->response($result);
613
        }
614
615
        return $this->response();
616
    }
617
618
    /**
619
     * Get and set response.
620
     *
621
     * @param \Cake\Network\Response $response  A Response object.
622
     * @return \Cake\Network\Response
623
     */
624
    public function response($response = null)
625
    {
626 8
        if ($response === null) {
627
            return $this->_response;
628 8
        }
629 8
630 8
        $this->_response = $response;
631 8
632 8
        return $this->_response;
633 8
    }
634 8
635 8
    /**
636 8
     * Service renderer configuration method.
637 8
     *
638
     * @param BaseRenderer $renderer A Renderer instance.
639
     * @return BaseRenderer
640
     */
641
    public function renderer(BaseRenderer $renderer = null)
642
    {
643
        if ($renderer === null) {
644
            return $this->_renderer;
645 118
        }
646
        $this->_renderer = $renderer;
647 118
648
        return $this->_renderer;
649
    }
650 118
651 118
    /**
652 118
     * Define action config.
653 118
     *
654
     * @param string $actionName Action name.
655 118
     * @param string $className Class name.
656 118
     * @param array $route Route config.
657
     * @return void
658
     */
659 118
    public function mapAction($actionName, $className, $route)
660 118
    {
661
        $route += ['mapCors' => false];
662
        $this->_actionsClassMap[$actionName] = $className;
663
        if ($route['mapCors']) {
664
            $this->_actionsClassMap[$actionName . $this->_corsSuffix] = DummyAction::class;
665
        }
666
        if (!isset($route['path'])) {
667
            $route['path'] = $actionName;
668 118
        }
669
        $this->_actions[$actionName] = $route;
670 118
    }
671 13
	
672 13
    /**
673 118
     * @return array
674 118
     */
675 105
    public function implementedEvents()
676 105
    {
677
        $eventMap = [
678 118
            'Service.beforeDispatch' => 'beforeDispatch',
679 118
            'Service.beforeProcess' => 'beforeProcess',
680
            'Service.afterDispatch' => 'afterDispatch',
681
        ];
682 118
        $events = [];
683 118
684
        foreach ($eventMap as $event => $method) {
685
            if (!method_exists($this, $method)) {
686
                continue;
687
            }
688
            $events[$event] = $method;
689
        }
690
691
        return $events;
692
    }
693
694
    /**
695
     * Get the extension registry for this service.
696
     *
697
     * If called with the first parameter, it will be set as the action $this->_extensions property
698
     *
699
     * @param \CakeDC\Api\Service\ExtensionRegistry|null $extensions Extension registry.
700
     *
701
     * @return \CakeDC\Api\Service\ExtensionRegistry
702
     */
703
    public function extensions($extensions = null)
704
    {
705
        if ($extensions === null && $this->_extensions === null) {
706
            $this->_extensions = new ExtensionRegistry($this);
707
        }
708
        if ($extensions !== null) {
709
            $this->_extensions = $extensions;
710
        }
711
712
        return $this->_extensions;
713
    }
714
715
    /**
716
     * Loads the defined extensions using the Extension factory.
717
     *
718
     * @return void
719
     */
720
    protected function _loadExtensions()
721
    {
722
        if (empty($this->extensions)) {
723
            return;
724
        }
725
        $registry = $this->extensions();
726
        $extensions = $registry->normalizeArray($this->extensions);
727
        foreach ($extensions as $properties) {
728
            $instance = $registry->load($properties['class'], $properties['config']);
729
            $this->_eventManager->on($instance);
730
        }
731
    }	
732
733
    /**
734
     * Initialize parser.
735
     *
736
     * @param array $config Service options
737
     * @return void
738
     */
739
    protected function _initializeParser(array $config)
740
    {
741
        if (empty($this->_parserClass) && isset($config['parserClass'])) {
742
            $this->_parserClass = $config['parserClass'];
743
        }
744
        $parserClass = Configure::read('Api.parser');
745
        if (empty($this->_parserClass) && !empty($parserClass)) {
746
            $this->_parserClass = $parserClass;
747
        }
748
749
        $class = App::className($this->_parserClass, 'Service/RequestParser', 'Parser');
750
        if (!class_exists($class)) {
751
            throw new MissingParserException(['class' => $this->_parserClass]);
752
        }
753
        $this->_parser = new $class($this);
754
    }
755
756
    /**
757
     * Initialize renderer.
758
     *
759
     * @param array $config Service options.
760
     * @return void
761
     */
762
    protected function _initializeRenderer(array $config)
763
    {
764
        if (empty($this->_rendererClass) && isset($config['rendererClass'])) {
765
            $this->_rendererClass = $config['rendererClass'];
766
        }
767
        $rendererClass = Configure::read('Api.renderer');
768
        if (empty($this->_rendererClass) && !empty($rendererClass)) {
769
            $this->_rendererClass = $rendererClass;
770
        }
771
772
        $class = App::className($this->_rendererClass, 'Service/Renderer', 'Renderer');
773
        if (!class_exists($class)) {
774
            throw new MissingRendererException(['class' => $this->_rendererClass]);
775
        }
776
        $this->_renderer = new $class($this);
777
    }
778
}
779