Issues (61)

src/WebServCo/Framework/AbstractApplication.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebServCo\Framework;
6
7
use WebServCo\Framework\Exceptions\NonApplicationException;
8
use WebServCo\Framework\Helpers\PhpHelper;
9
use WebServCo\Framework\Interfaces\ResponseInterface;
10
11
abstract class AbstractApplication
12
{
13
    use \WebServCo\Framework\Traits\ExposeLibrariesTrait;
14
15
    protected string $projectNamespace;
16
    protected string $projectPath;
17
18
    abstract protected function getCliOutput(\Throwable $throwable): string;
19
20
    abstract protected function getHttpOutput(\Throwable $throwable): string;
21
22
    abstract protected function getResponse(): ResponseInterface;
23
24
    public function __construct(string $publicPath, ?string $projectPath, ?string $projectNamespace)
25
    {
26
        // If no custom namespace is set, use "Project".
27
        $this->projectNamespace = $projectNamespace ?? 'Project';
28
29
        // Make sure path ends with a slash.
30
        $publicPath = \rtrim($publicPath, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
31
32
        if (!\is_readable($publicPath . 'index.php')) {
33
            throw new NonApplicationException('Public web path is not readable.');
34
        }
35
36
        // If no custom project path is set, use parent directory of public.
37
        $projectPath ??= (string) \realpath(\sprintf('%s..', $publicPath));
38
39
        // Make sure path ends with a slash.
40
        $this->projectPath = \rtrim($projectPath, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR;
41
42
        // Add environment settings.
43
44
        \WebServCo\Framework\Environment\Setting::set('APP_PATH_WEB', $publicPath);
45
        \WebServCo\Framework\Environment\Setting::set('APP_PATH_PROJECT', $this->projectPath);
46
        \WebServCo\Framework\Environment\Setting::set('APP_PATH_LOG', \sprintf('%svar/log/', $this->projectPath));
47
    }
48
49
    /**
50
     * Runs the application.
51
     */
52
    final public function run(): void
53
    {
54
        try {
55
            ErrorHandler::set();
56
57
            \register_shutdown_function([$this, 'shutdown']);
58
59
            $this->loadEnvironmentSettings();
60
61
            $response = $this->getResponse();
62
63
            // Handle errors that happen before PHP script execution (so before the error handler is registered)
64
            // Call helper with no exception as parameter.
65
            // Helper will check for last error and return an \ErrorException object.
66
            // This code after `execute` and before `send` in order to give the app a chance to handle this situation.
67
            $throwable = \WebServCo\Framework\Helpers\ErrorObjectHelper::get(null);
68
            if ($throwable instanceof \Throwable) {
0 ignored issues
show
$throwable is always a sub-type of Throwable.
Loading history...
69
                throw new NonApplicationException($throwable->getMessage(), $throwable->getCode(), $throwable);
70
            }
71
72
            $statusCode = $response->send();
73
74
            //$this->shutdown(null, true, PhpHelper::isCli() ? $statusCode : 0);
75
            $this->shutdown(null, true, $statusCode);
76
        } catch (\Throwable $e) {
77
            $this->shutdown($e, true);
78
        }
79
    }
80
81
    /**
82
     * Finishes the execution of the Application.
83
     *
84
     * This method is also registered as a shutdown handler.
85
     */
86
    final public function shutdown(?\Throwable $exception = null, bool $manual = false, int $statusCode = 0): void
87
    {
88
        $hasError = $this->handleErrors($exception);
89
        if ($hasError) {
0 ignored issues
show
The condition $hasError is always true.
Loading history...
90
            $statusCode = 1;
91
        }
92
93
        if (!$manual) { // if shutdown handler
94
            /**
95
             * Warning: this part will always be executed,
96
             * independent of the outcome of the script.
97
             */
98
            ErrorHandler::restore();
99
        }
100
        exit($statusCode);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
101
    }
102
103
    final protected function execute(): ResponseInterface
104
    {
105
        $classType = PhpHelper::isCli()
106
            ? 'Command'
107
            : 'Controller';
108
        $target = $this->request()->getTarget();
109
        // \WebServCo\Framework\Objects\Route
110
        $route = $this->router()->getRoute(
111
            $target,
112
            $this->router()->setting('routes', []),
113
            $this->request()->getArgs(),
114
        );
115
116
        $className = \sprintf("\\%s\\Domain\\%s\\%s", $this->projectNamespace, $route->class, $classType);
117
        if (!\class_exists($className)) {
118
            if ('Controller' !== $classType) {
119
                throw new NonApplicationException(
120
                    \sprintf('No matching %s found. Target: "%s"', $classType, $target),
121
                );
122
            }
123
            // Class type is "Controller", so check for 404 route
124
            // throws \WebServCo\Framework\Exceptions\NotFoundException
125
            $route = $this->router()->getFourOhfourRoute();
126
            $className = \sprintf("\\%s\\Domain\\%s\\%s", $this->projectNamespace, $route->class, $classType);
127
        }
128
129
        $object = new $className();
130
        $parent = \get_parent_class($object);
131
        /**
132
         * PHP 8 compatibility.
133
         *
134
         * https://www.php.net/manual/en/migration80.incompatible.php
135
         * "The ability to call non-static methods statically has been removed.
136
         * Thus is_callable() will fail when checking for a non-static method with a classname
137
         * (must check with an object instance). "
138
         * old line:
139
         * if (\method_exists((string) $parent, $route->method) || !\is_callable([$className, $route->method])) {
140
         */
141
        if (\method_exists((string) $parent, $route->method) || !\method_exists($className, $route->method)) {
142
            throw new NonApplicationException(\sprintf('No valid matching Action found. Target: "%s".', $target));
143
        }
144
        $callable = [$object, $route->method];
145
        if (!\is_callable($callable)) {
146
            throw new NonApplicationException(\sprintf('Method is not callable. Target: "%s"', $target));
147
        }
148
149
        return \call_user_func_array($callable, $route->arguments);
150
    }
151
152
    /**
153
     * Handle Errors.
154
     */
155
    final protected function handleErrors(?\Throwable $exception = null): bool
156
    {
157
        $throwable = \WebServCo\Framework\Helpers\ErrorObjectHelper::get($exception);
158
        if ($throwable instanceof \Throwable) {
0 ignored issues
show
$throwable is always a sub-type of Throwable.
Loading history...
159
            return $this->halt($throwable);
160
        }
161
        return false;
162
    }
163
164
    final protected function halt(\Throwable $throwable): bool
165
    {
166
        return \WebServCo\Framework\Helpers\PhpHelper::isCli()
167
            ? $this->haltCli($throwable)
168
            : $this->haltHttp($throwable);
169
    }
170
171
    protected function haltCli(\Throwable $throwable): bool
172
    {
173
        $output = $this->getCliOutput($throwable);
174
        $response = new \WebServCo\Framework\Cli\Response($output, 1);
175
        $response->send();
176
        return true;
177
    }
178
179
    protected function haltHttp(\Throwable $throwable): bool
180
    {
181
        $output = $this->getHttpOutput($throwable);
182
183
        $code = $throwable->getCode();
184
        switch ($code) {
185
            case 0: // non-HTTP
186
            case -1: // non-HTTP
187
                $statusCode = 500;
188
                break;
189
            default:
190
                $statusCodes = \WebServCo\Framework\Http\StatusCode::getSupported();
191
                $statusCode = \array_key_exists($code, $statusCodes)
192
                    ? $code //  Use Throwable status code as it is a valid HTTP code
193
                    : 500;
194
                break;
195
        }
196
197
        $response = new \WebServCo\Framework\Http\Response(
198
            $output,
199
            $statusCode,
200
            ['Content-Type' => ['text/html']],
201
        );
202
        $response->send();
203
        return true;
204
    }
205
206
    protected function loadEnvironmentSettings(): bool
207
    {
208
        return \WebServCo\Framework\Environment\Settings::load($this->projectPath);
209
    }
210
}
211