Manager::execute()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 12
cts 12
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * This file is part of the Drest package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @author Lee Davis
9
 * @copyright Copyright (c) Lee Davis <@leedavis81>
10
 * @link https://github.com/leedavis81/drest/blob/master/LICENSE
11
 * @license http://opensource.org/licenses/MIT The MIT X License (MIT)
12
 */
13
namespace Drest;
14
15
use Drest\Mapping\RouteMetaData;
16
use Drest\Route\MultipleRoutesException;
17
use Drest\Route\NoMatchException;
18
use Drest\Service\Action\Registry as ServiceActionRegistry;
19
use DrestCommon\Error\Handler\AbstractHandler;
20
use DrestCommon\Error\Response\Text as ErrorResponseText;
21
use DrestCommon\Representation\UnableToMatchRepresentationException;
22
use DrestCommon\Request\Request;
23
use DrestCommon\Response\Response;
24
25
class Manager
26
{
27
    use HttpManagerTrait;
28
    use EventManagerTrait;
29
30
    /**
31
     * Doctrine Entity Manager Registry
32
     * @var EntityManagerRegistry $emr
33
     */
34
    protected $emr;
35
36
    /**
37
     * Drest configuration object
38
     * @var Configuration $config
39
     */
40
    protected $config;
41
42
    /**
43
     * Metadata manager object
44
     * @var Manager\Metadata $metadataManager
45
     */
46
    protected $metadataManager;
47
48
    /**
49
     * Representation manager
50
     * @var Manager\Representation
51
     */
52
    protected $representationManager;
53
54
    /**
55
     * Drest router
56
     * @var \Drest\Router $router
57
     */
58
    protected $router;
59
60
    /**
61
     * A service object used to handle service actions
62
     * @var Service $service
63
     */
64
    protected $service;
65
66
    /**
67
     * Error handler object
68
     * @var AbstractHandler $error_handler
69
     */
70
    protected $error_handler;
71
72
    /**
73
     * The name of an explicit route call
74
     * @var string $named_route
75
     */
76
    protected $named_route;
77
78
    /**
79
     * Optional route parameter that may have been passed
80
     * @var array $route_params
81
     */
82
    protected $route_params;
83
84
    /**
85
     * Creates an instance of the Drest Manager using the passed configuration object
86
     * Can also pass in a Event Manager instance
87
     *
88
     * @param EntityManagerRegistry   $entityManagerRegistry
89
     * @param Configuration           $config
90
     * @param Event\Manager           $eventManager
91
     * @param ServiceActionRegistry   $serviceActionRegistry
92
     */
93 31
    private function __construct(
94
        EntityManagerRegistry $entityManagerRegistry,
95
        Configuration $config,
96
        Event\Manager $eventManager,
97
        ServiceActionRegistry $serviceActionRegistry
98
    )
99
    {
100 31
        $this->emr = $entityManagerRegistry;
101 31
        $this->config = $config;
102 31
        $this->eventManager = $eventManager;
103 31
        $this->service = new Service($this, $serviceActionRegistry);
104
105
        // Router is internal and currently cannot be injected / extended
106 31
        $this->router = new Router();
107
108 31
        $this->metadataManager = Manager\Metadata::create($config);
109 31
        $this->representationManager = Manager\Representation::create($this->config);
110 31
    }
111
112
    /**
113
     * Static call to create the Drest Manager instance
114
     *
115
     * @param  EntityManagerRegistry    $entityManagerRegistry
116
     * @param  Configuration            $config
117
     * @param  Event\Manager|null       $eventManager
118
     * @param  ServiceActionRegistry  $serviceActionRegistry
119
     * @return Manager                  $manager
120
     */
121 31
    public static function create(
122
        EntityManagerRegistry $entityManagerRegistry,
123
        Configuration $config,
124
        Event\Manager $eventManager = null,
125
        ServiceActionRegistry $serviceActionRegistry = null
126
    )
127
    {
128 31
        $driver = $config->getMetadataDriverClass();
129
130 31
        if(method_exists($driver, 'register')) {
131 31
            $driver::register($config);
132 31
        }
133
134 31
        return new self(
135 31
            $entityManagerRegistry,
136 31
            $config,
137 31
            ($eventManager) ?: new Event\Manager(),
138 31
            ($serviceActionRegistry) ?: new Service\Action\Registry()
139 31
        );
140
    }
141
142
143
    /**
144
     * Dispatch a REST request
145
     * @param  object     $request     - Framework request object
146
     * @param  object     $response    - Framework response object
147
     * @param  string     $namedRoute  - Define the named Route to be dispatch - by passes the internal router
148
     * @param  array      $routeParams - Route parameters to be used when dispatching a namedRoute request
149
     * @return Response   $response - returns a Drest response object which can be sent calling toString()
150
     * @throws \Exception - Upon failure
151
     */
152 31
    public function dispatch($request = null, $response = null, $namedRoute = null, array $routeParams = [])
153
    {
154 31
        $this->setUpHttp($request, $response, $this->getConfiguration());
155
156
        // Save the named route to the object (overwritten on each dispatch)
157 31
        $this->named_route = $namedRoute;
158 31
        $this->route_params = $routeParams;
159
160
        // Register routes for lookup
161 31
        $this->metadataManager->registerRoutes($this->router);
162
163
        // trigger preDispatch event
164 31
        $this->triggerPreDispatchEvent($this->service);
165
166
        try {
167 31
            $this->execute();
168 31
        } catch (\Exception $e) {
169
170 4
            if ($this->config->inDebugMode()) {
171
                // trigger a postDispatch event
172 3
                $this->triggerPostDispatchEvent($this->service);
173 3
                throw $e;
174
            }
175 1
            $this->handleError($e);
176
        }
177
178
        // trigger a postDispatch event
179 28
        $this->triggerPostDispatchEvent($this->service);
180
181 28
        return $this->getResponse();
182
    }
183
184
    /**
185
     * Execute a dispatched request
186
     * @throws Route\NoMatchException|\Exception
187
     */
188 31
    protected function execute()
189
    {
190 31
        if (($route = $this->determineRoute()) instanceof RouteMetaData) {
191
192
            // Set the matched service object and the error handler into the service class
193 25
            $this->service->setMatchedRoute($route);
194
            // Get the representation to be used - always successful or it throws an exception
195 25
            $this->service->setRepresentation($this->representationManager->handleExposureSettingsFromHttpMethod(
196 25
                $this->getRequest(),
197 25
                $route,
198 25
                $this->emr
199 25
            ));
200 25
            $this->service->setErrorHandler($this->getErrorHandler());
201
202
            // Set up the service for a new request
203 25
            $this->service->setUpAndRunRequest();
204 25
        }
205 27
    }
206
207
208
    /**
209
     * Determine the matched route from either the router or namedRoute
210
     * Returns false no route could be matched (ideally the response should be returned in this instance - fail fast)
211
     * @throws Route\NoMatchException|\Exception
212
     * @return RouteMetaData|false $route
213
     */
214 31
    protected function determineRoute()
215
    {
216
        // dispatch preRoutingAction event
217 31
        $this->triggerPreRoutingEvent($this->service);
218
        try {
219 31
            $route = (!is_null($this->named_route))
220 31
                ? $this->getNamedRoute()
221 30
                : $this->getMatchedRoute(true);
222 31
        } catch (\Exception $e) {
223
224
            // dispatch postRoutingAction event
225 6
            $this->triggerPostRoutingEvent($this->service);
226
227 6
            if ($e instanceof NoMatchException &&
228 5
                ($this->doCGOptionsCheck() || $this->doOptionsCheck())
229 6
            ) {
230 2
                return false;
231
            }
232 4
            throw $e;
233
        }
234
235
        // check push edges have a handle, or a registered service action (also, only do this when we match on them)
236 25
        if ($route->needsHandleCall() &&
237 3
                (!$route->hasHandleCall() &&
238
                 !$this->service->getServiceActionRegistry()->hasServiceAction($route))
239 3
        )
240 25
        {
241
            throw DrestException::routeRequiresHandle($route->getName());
242
        }
243
244
245
        // dispatch postRoutingAction event
246 25
        $this->triggerPostRoutingEvent($this->service);
247
248
        // Set parameters matched on the route to the request object
249 25
        $this->getRequest()->setRouteParam($route->getRouteParams());
250
251 25
        return $route;
252
    }
253
254
    /**
255
     * Get a route based on Entity::route_name. eg Entities\User::get_users
256
     * Syntax checking is performed
257
     * @throws DrestException on invalid syntax or unmatched named route
258
     * @return RouteMetaData  $route
259
     */
260 3
    protected function getNamedRoute()
261
    {
262 3
        if (substr_count($this->named_route, '::') !== 1) {
263
            throw DrestException::invalidNamedRouteSyntax();
264
        }
265 3
        $parts = explode('::', $this->named_route);
266
267
        // Allow exception to bubble up
268 3
        $classMetaData = $this->getClassMetadata($parts[0]);
269 3
        if (($route = $classMetaData->getRouteMetaData($parts[1])) === false) {
270 1
            throw DrestException::unableToFindRouteByName($parts[1], $classMetaData->getClassName());
271
        }
272
273 2
        $route->setRouteParams($this->route_params);
274
275 2
        return $route;
276
    }
277
278
    /**
279
     * Was the last dispatch request called with a named route?
280
     * @return bool
281
     */
282 25
    public function calledWithANamedRoute()
283
    {
284 25
        return !is_null($this->named_route);
285
    }
286
287
    /**
288
     * Check if the client has requested the CG classes with an OPTIONS call
289
     * @return bool
290
     */
291 5
    protected function doCGOptionsCheck()
292
    {
293 5
        $genClasses = $this->getRequest()->getHeaders(ClassGenerator::HEADER_PARAM);
294 5
        if ($this->getRequest()->getHttpMethod() != Request::METHOD_OPTIONS || empty($genClasses)) {
295 4
            return false;
296
        }
297
298 1
        $classGenerator = new ClassGenerator($this->emr);
299
300 1
        $classMetadatas = [];
301 1
        if (!empty($genClasses)) {
302 1
            foreach ($this->metadataManager->getAllClassNames() as $className) {
303 1
                $metaData = $this->getClassMetadata($className);
304 1
                foreach ($metaData->getRoutesMetaData() as $route) {
305
                    /* @var RouteMetaData $route */
306 1
                    $route->setExpose(
307 1
                        Query\ExposeFields::create($route)
308 1
                            ->configureExposeDepth(
309 1
                                $this->emr,
310 1
                                $this->config->getExposureDepth(),
311 1
                                $this->config->getExposureRelationsFetchType()
312 1
                            )
313 1
                            ->toArray()
314 1
                    );
315 1
                }
316 1
                $classMetadatas[] = $metaData;
317 1
            }
318 1
        }
319
320 1
        $classGenerator->create($classMetadatas);
321
322 1
        $this->getResponse()->setBody($classGenerator->serialize());
323
324 1
        return true;
325
    }
326
327
    /**
328
     * No match on route has occurred. Check the HTTP verb used for an options response
329
     * Returns true if it is, and option information was successfully written to the response object
330
     * @return boolean $success
331
     */
332 4
    protected function doOptionsCheck()
333
    {
334 4
        if ($this->getRequest()->getHttpMethod() != Request::METHOD_OPTIONS) {
335 2
            return false;
336
        }
337
338
        // Do a match on all routes - don't include a verb check
339 2
        $verbs = [];
340 2
        foreach ($this->getMatchedRoutes(false) as $route) {
341
            /* @var RouteMetaData $route */
342 2
            $allowedOptions = $route->isAllowedOptionRequest();
343 2
            if (false === (($allowedOptions === -1)
344 2
                    ? $this->config->getAllowOptionsRequest()
345 2
                    : (bool) $allowedOptions)
346 2
            ) {
347 1
                continue;
348
            }
349 1
            $verbs = array_merge($verbs, $route->getVerbs());
350 2
        }
351
352 2
        if (empty($verbs)) {
353 1
            return false;
354
        }
355
356 1
        $this->getResponse()->setHttpHeader('Allow', implode(', ', $verbs));
357
358 1
        return true;
359
    }
360
361
    /**
362
     * Runs through all the registered routes and returns a single match
363
     * @param  boolean                 $matchVerb - Whether you want to match the route using the request HTTP verb
364
     * @throws NoMatchException        if no routes are found
365
     * @throws MultipleRoutesException If there are multiple matches
366
     * @return RouteMetaData           $route
367
     */
368 28
    protected function getMatchedRoute($matchVerb = true)
369
    {
370
        // Inject any route base Paths that have been registered
371 28
        if ($this->config->hasRouteBasePaths()) {
372
            $this->router->setRouteBasePaths($this->config->getRouteBasePaths());
373
        }
374
375 28
        $matchedRoutes = $this->router->getMatchedRoutes($this->getRequest(), (bool) $matchVerb);
376 28
        if (sizeof($matchedRoutes) == 0) {
377 5
            throw NoMatchException::noMatchedRoutes();
378 23
        } elseif (sizeof($matchedRoutes) > 1) {
379
            throw MultipleRoutesException::multipleRoutesFound($matchedRoutes);
380
        }
381
382 23
        return $matchedRoutes[0];
383
    }
384
385
    /**
386
     * Get all possible match routes for this request
387
     * @param  boolean $matchVerb - Whether you want to match the route using the request HTTP verb
388
     * @return array   of Drest\Mapping\RouteMetaData object
389
     */
390 2
    protected function getMatchedRoutes($matchVerb = true)
391
    {
392 2
        return $this->router->getMatchedRoutes($this->getRequest(), (bool) $matchVerb);
393
    }
394
395
    /**
396
     * Handle an error by passing the exception to the registered error handler
397
     * @param  \Exception $e
398
     * @throws \Exception
399
     */
400 1
    private function handleError(\Exception $e)
401
    {
402 1
        $eh = $this->getErrorHandler();
403
404
        try {
405 1
            $representation = $this->representationManager->getDeterminedRepresentation($this->getRequest());
406 1
            $errorDocument = $representation->getDefaultErrorResponse();
407 1
            $eh->error($e, 500, $errorDocument);
408 1
        } catch (UnableToMatchRepresentationException $e) {
409
            $errorDocument = new ErrorResponseText();
410
            $eh->error($e, 500, $errorDocument);
411
        }
412
413 1
        $this->getResponse()->setStatusCode($eh->getResponseCode());
414 1
        $this->getResponse()->setHttpHeader('Content-Type', $errorDocument::getContentType());
415 1
        $this->getResponse()->setBody($errorDocument->render());
416 1
    }
417
418
    /**
419
     * Get Configuration object used
420
     * @return Configuration
421
     */
422 31
    public function getConfiguration()
423
    {
424 31
        return $this->config;
425
    }
426
427
    /**
428
     * Get the entity manager registry
429
     * @return EntityManagerRegistry
430
     */
431 25
    public function getEntityManagerRegistry()
432
    {
433 25
        return $this->emr;
434
    }
435
436
    /**
437
     * Get the service action registry
438
     * @return ServiceActionRegistry
439
     */
440
    public function getServiceActionRegistry()
441
    {
442
        return $this->service->getServiceActionRegistry();
443
    }
444
445
    /**
446
     * Get the error handler object, if none has been injected use default from config
447
     * @return AbstractHandler $error_handler
448
     */
449 26
    public function getErrorHandler()
450
    {
451 26
        if (!$this->error_handler instanceof AbstractHandler) {
452
            // Force creation of an instance of the default error handler
453 26
            $className = $this->config->getDefaultErrorHandlerClass();
454 26
            $this->error_handler = new $className();
455 26
        }
456
457 26
        return $this->error_handler;
458
    }
459
460
    /**
461
     * Set the error handler to use
462
     * @param AbstractHandler $error_handler
463
     */
464
    public function setErrorHandler(AbstractHandler $error_handler)
465
    {
466
        $this->error_handler = $error_handler;
467
    }
468
469
    /**
470
     * Get metadata for an entity class
471
     * @param  string                   $className
472
     * @return Mapping\ClassMetaData
473
     */
474 4
    public function getClassMetadata($className)
475
    {
476 4
        return $this->metadataManager->getMetadataForClass($className);
477
    }
478
479
    /**
480
     * Iterates through mapping definitions, any exceptions thrown will bubble up.
481
     */
482
    public function checkDefinitions()
483
    {
484
        foreach ($this->metadataManager->getAllClassNames() as $class) {
485
            $this->getClassMetadata($class);
486
        }
487
    }
488
}
489