Failed Conditions
Push — master ( c5c7e6...8b5bba )
by Adrien
17:22 queued 03:55
created

Server::handleError()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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