Issues (51)

src/Api/Server.php (2 issues)

Labels
Severity
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\Error\UserError;
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 8
    public function __construct(Schema $schema, bool $debug, array $rootValue = [])
32
    {
33 8
        GraphQL::setDefaultFieldResolver(new FilteredFieldResolver());
34
35 8
        $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
36
37 8
        $this->config = ServerConfig::create([
0 ignored issues
show
The property config is declared read-only in Ecodev\Felix\Api\Server.
Loading history...
38 8
            'schema' => $schema,
39 8
            'queryBatching' => true,
40 8
            'debugFlag' => $debug ? $debugFlag : DebugFlag::NONE,
41 8
            'errorsHandler' => function (array $errors, callable $formatter) {
42 4
                $result = [];
43 4
                foreach ($errors as $e) {
44 4
                    $result[] = $this->handleError($e, $formatter);
45
                }
46
47 4
                return $result;
48 8
            },
49 8
            'rootValue' => $rootValue,
50 8
        ]);
51
52 8
        $this->server = new StandardServer($this->config);
0 ignored issues
show
The property server is declared read-only in Ecodev\Felix\Api\Server.
Loading history...
53
    }
54
55
    /**
56
     * @return ExecutionResult|ExecutionResult[]
57
     */
58 8
    public function execute(ServerRequestInterface $request): array|ExecutionResult
59
    {
60 8
        if (!$request->getParsedBody()) {
61
            /** @var array $parsedBody */
62 8
            $parsedBody = json_decode($request->getBody()->getContents(), true) ?? [];
63 8
            $request = $request->withParsedBody($parsedBody);
64
        }
65
66
        // Affect it to global request, so it is available for log purpose in case of error
67 8
        $_REQUEST = $request->getParsedBody();
68
69
        // Set current session as the only context we will ever need
70 8
        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
71 8
        $this->config->setContext($session);
72
73 8
        return $this->server->executePsrRequest($request);
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 4
    private function handleError(Throwable $exception, callable $formatter): array
88
    {
89
        // Always log exception in DB (and by email)
90 4
        _log()->error($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 4
        $previous = $exception->getPrevious();
95 4
        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 4
        $result = $formatter($exception);
104
105
        // Invalid variable that end-user might have crafted via the URL
106 4
        $isInvalidVariables = preg_match('~^Variable \".*\" got invalid value ~', $exception->getMessage());
107 4
        $isFelixException = $exception->getPrevious() instanceof Exception;
108 4
        if ($isFelixException || $isInvalidVariables) {
109 2
            $result['extensions']['showSnack'] = true;
110
        }
111
112
        // Not found object
113 4
        if ($exception->getPrevious() instanceof UserError && preg_match('~^Entity not found for class `~', $exception->getMessage())) {
114 1
            $result['extensions']['objectNotFound'] = true;
115
        }
116
117 4
        return $result;
118
    }
119
}
120