Passed
Push — master ( 605651...ecc05d )
by Adrien
03:06
created

Server::__construct()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.3731

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 16
c 4
b 0
f 0
dl 0
loc 28
ccs 10
cts 14
cp 0.7143
rs 9.7333
cc 4
nc 2
nop 3
crap 4.3731
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Api;
6
7
use Doctrine\DBAL\Exception\DriverException;
8
use GraphQL\Error\DebugFlag;
9
use GraphQL\Executor\ExecutionResult;
10
use GraphQL\GraphQL;
11
use GraphQL\Server\ServerConfig;
12
use GraphQL\Server\StandardServer;
13
use GraphQL\Type\Schema;
14
use GraphQL\Validator\DocumentValidator;
15
use GraphQL\Validator\Rules\DisableIntrospection;
16
use Mezzio\Session\SessionMiddleware;
17
use Psr\Http\Message\ServerRequestInterface;
18
use Throwable;
19
20
/**
21
 * A thin wrapper to serve GraphQL via HTTP or CLI.
22
 */
23
class Server
24
{
25
    private readonly StandardServer $server;
26
27
    private readonly ServerConfig $config;
28
29
    /**
30
     * @param bool $debug if true, allows the introspection query, and dumps stacktrace in case of error
31
     */
32 4
    public function __construct(Schema $schema, bool $debug, array $rootValue = [])
33
    {
34 4
        GraphQL::setDefaultFieldResolver(new FilteredFieldResolver());
35
36 4
        $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
37
38
        // Forbid introspection query in production mode, because our API is not meant to be publicly available
39 4
        if (!$debug) {
40 4
            $rule = new DisableIntrospection(DisableIntrospection::ENABLED);
41 4
            DocumentValidator::addRule($rule);
42
        }
43
44 4
        $this->config = ServerConfig::create([
0 ignored issues
show
Bug introduced by
The property config is declared read-only in Ecodev\Felix\Api\Server.
Loading history...
45
            'schema' => $schema,
46
            'queryBatching' => true,
47 4
            'debugFlag' => $debug ? $debugFlag : DebugFlag::NONE,
48 4
            'errorsHandler' => function (array $errors, callable $formatter) {
49
                $result = [];
50
                foreach ($errors as $e) {
51
                    $result[] = $this->handleError($e, $formatter);
52
                }
53
54
                return $result;
55
            },
56
            'rootValue' => $rootValue,
57
        ]);
58
59 4
        $this->server = new StandardServer($this->config);
0 ignored issues
show
Bug introduced by
The property server is declared read-only in Ecodev\Felix\Api\Server.
Loading history...
60
    }
61
62
    /**
63
     * @return ExecutionResult|ExecutionResult[]
64
     */
65 4
    public function execute(ServerRequestInterface $request): array|ExecutionResult
66
    {
67 4
        if (!$request->getParsedBody()) {
68
            /** @var array $parsedBody */
69 4
            $parsedBody = json_decode($request->getBody()->getContents(), true) ?? [];
70 4
            $request = $request->withParsedBody($parsedBody);
71
        }
72
73
        // Affect it to global request, so it is available for log purpose in case of error
74 4
        $_REQUEST = $request->getParsedBody();
75
76
        // Set current session as the only context we will ever need
77 4
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
78 4
        $this->config->setContext($session);
79
80 4
        return $this->server->executePsrRequest($request);
1 ignored issue
show
Bug Best Practice introduced by
The expression return $this->server->executePsrRequest($request) could return the type GraphQL\Executor\Promise\Promise which is incompatible with the type-hinted return GraphQL\Executor\ExecutionResult|array. Consider adding an additional type-check to rule them out.
Loading history...
81
    }
82
83
    /**
84
     * Send response to CLI.
85
     */
86
    public function sendCli(ExecutionResult $result): void
87
    {
88
        echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL;
89
    }
90
91
    /**
92
     * Custom error handler to log in DB and show trigger messages to end-user.
93
     */
94
    private function handleError(Throwable $exception, callable $formatter): array
95
    {
96
        // Always log exception in DB (and by email)
97
        _log()->err($exception->__toString(), ['exception' => $exception]);
98
99
        // If we are absolutely certain that the error comes from one of our trigger with a custom message for end-user,
100
        // then wrap the exception to make it showable to the end-user
101
        $previous = $exception->getPrevious();
102
        if ($previous instanceof DriverException && $previous->getSQLState() === '45000' && $previous->getPrevious() && $previous->getPrevious()->getPrevious()) {
103
            $message = $previous->getPrevious()->getPrevious()->getMessage();
104
            $userMessage = (string) preg_replace('~SQLSTATE\[45000]: <<Unknown error>>: \d+ ~', '', $message, -1, $count);
105
            if ($count === 1) {
106
                $exception = new Exception($userMessage, 0, $exception);
107
            }
108
        }
109
110
        return $formatter($exception);
111
    }
112
}
113