Passed
Push — master ( 47c66e...4495e8 )
by Mr
02:43
created

App::runClass()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 30
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 3
nop 4
dl 0
loc 30
rs 8.5806
c 0
b 0
f 0
1
<?php
2
3
namespace DrMVC\Framework;
4
5
use Psr\Http\Message\RequestInterface;
6
use Psr\Http\Message\ResponseInterface;
7
use Psr\Http\Message\StreamInterface;
8
9
use Zend\Diactoros\ServerRequestFactory;
10
use Zend\Diactoros\ServerRequest;
11
use Zend\Diactoros\Response;
12
13
use DrMVC\Router\Router;
14
use DrMVC\Router\RouteInterface;
15
use DrMVC\Router\MethodsInterface;
16
use DrMVC\Config\ConfigInterface;
17
18
/**
19
 * Class App
20
 * @package DrMVC\Framework
21
 * @method App options(string $pattern, callable $callable): App
22
 * @method App get(string $pattern, callable $callable): App
23
 * @method App head(string $pattern, callable $callable): App
24
 * @method App post(string $pattern, callable $callable): App
25
 * @method App put(string $pattern, callable $callable): App
26
 * @method App delete(string $pattern, callable $callable): App
27
 * @method App trace(string $pattern, callable $callable): App
28
 * @method App connect(string $pattern, callable $callable): App
29
 * @since 3.0
30
 */
31
class App implements AppInterface
32
{
33
    const DEFAULT_ACTION = 'index';
34
35
    /**
36
     * @var ContainersInterface
37
     */
38
    private $_containers;
39
40
    /**
41
     * App constructor.
42
     * @param ConfigInterface $config
43
     */
44
    public function __construct(ConfigInterface $config)
45
    {
46
        // Initiate PSR-11 containers
47
        $this->initContainers();
48
49
        // Save configuration
50
        $this->initConfig($config);
51
52
        // Initiate router
53
        $this
54
            ->initRequest()
55
            ->initResponse()
56
            ->initRouter();
57
    }
58
59
    /**
60
     * Initialize containers object
61
     *
62
     * @return  App
63
     */
64
    private function initContainers(): App
65
    {
66
        if (null === $this->_containers) {
67
            $this->_containers = new Containers();
68
        }
69
        return $this;
70
    }
71
72
    /**
73
     * Put config object into the containers class
74
     *
75
     * @param   ConfigInterface $config
76
     * @return  App
77
     */
78
    private function initConfig(ConfigInterface $config): App
79
    {
80
        $this->containers()->set('config', $config);
0 ignored issues
show
Bug introduced by
$config of type DrMVC\Config\ConfigInterface is incompatible with the type string expected by parameter $object of DrMVC\Framework\ContainersInterface::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

80
        $this->containers()->set('config', /** @scrutinizer ignore-type */ $config);
Loading history...
81
        return $this;
82
    }
83
84
    /**
85
     * Initiate PSR-7 request object
86
     *
87
     * @return  App
88
     */
89
    private function initRequest(): App
90
    {
91
        try {
92
            $request = ServerRequestFactory::fromGlobals();
93
            $this->containers()->set('request', $request);
0 ignored issues
show
Bug introduced by
$request of type Zend\Diactoros\ServerRequest is incompatible with the type string expected by parameter $object of DrMVC\Framework\ContainersInterface::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
            $this->containers()->set('request', /** @scrutinizer ignore-type */ $request);
Loading history...
94
        } catch (\InvalidArgumentException $e) {
95
            new Exception($e);
96
        }
97
        return $this;
98
    }
99
100
    /**
101
     * Initiate PSR-7 response object
102
     *
103
     * @return  App
104
     */
105
    private function initResponse(): App
106
    {
107
        try {
108
            $response = new Response();
109
            $this->containers()->set('response', $response);
0 ignored issues
show
Bug introduced by
$response of type Zend\Diactoros\Response is incompatible with the type string expected by parameter $object of DrMVC\Framework\ContainersInterface::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
            $this->containers()->set('response', /** @scrutinizer ignore-type */ $response);
Loading history...
110
        } catch (\InvalidArgumentException $e) {
111
            new Exception($e);
112
        }
113
        return $this;
114
    }
115
116
    /**
117
     * Put route into the container of classes
118
     *
119
     * @return  App
120
     */
121
    private function initRouter(): App
122
    {
123
        $request = $this->container('request');
124
        $response = $this->container('response');
125
        $router = new Router($request, $response);
126
        //$router->setError(Error::class);
127
128
        $this->containers()->set('router', $router);
0 ignored issues
show
Bug introduced by
$router of type DrMVC\Router\Router is incompatible with the type string expected by parameter $object of DrMVC\Framework\ContainersInterface::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

128
        $this->containers()->set('router', /** @scrutinizer ignore-type */ $router);
Loading history...
129
        return $this;
130
    }
131
132
    /**
133
     * Get custom container by name
134
     *
135
     * @param   string $name
136
     * @return  mixed
137
     */
138
    public function container(string $name)
139
    {
140
        return $this->_containers->get($name);
141
    }
142
143
    /**
144
     * Get all available containers
145
     *
146
     * @return  ContainersInterface
147
     */
148
    public function containers(): ContainersInterface
149
    {
150
        return $this->_containers;
151
    }
152
153
    /**
154
     * Magic method for work with calls
155
     *
156
     * @param   string $method
157
     * @param   array $args
158
     * @return  MethodsInterface
159
     */
160
    public function __call(string $method, array $args): MethodsInterface
161
    {
162
        if (\in_array($method, Router::METHODS, false)) {
163
            $this->container('router')->set([$method], $args);
164
        }
165
        return $this;
166
    }
167
168
    /**
169
     * If any route is provided
170
     *
171
     * @param   string $pattern
172
     * @param   callable|string $callable
173
     * @return  MethodsInterface
174
     */
175
    public function any(string $pattern, $callable): MethodsInterface
176
    {
177
        $this->container('router')->any($pattern, $callable);
178
        return $this;
179
    }
180
181
    /**
182
     * Set the error callback of class
183
     *
184
     * @param   callable|string $callable
185
     * @return  MethodsInterface
186
     */
187
    public function error($callable): MethodsInterface
188
    {
189
        $this->container('router')->error($callable);
190
        return $this;
191
    }
192
193
    /**
194
     * Few methods provided
195
     *
196
     * @param   array $methods
197
     * @param   string $pattern
198
     * @param   callable|string $callable
199
     * @return  MethodsInterface
200
     */
201
    public function map(array $methods, string $pattern, $callable): MethodsInterface
202
    {
203
        $this->container('router')->map($methods, $pattern, $callable);
204
        return $this;
205
    }
206
207
    /**
208
     * Detect action by string name, variable or use default
209
     *
210
     * @param   string $className - eg. MyApp\Index:test
211
     * @param   array $variables
212
     * @return  string
213
     */
214
    private function detectAction(string $className, array $variables = []): string
215
    {
216
        // If class contain method name
217
        if (strpos($className, ':') !== false) {
218
            $classArray = explode(':', $className);
219
            $classAction = end($classArray);
220
        } else {
221
            $classAction = null;
222
        }
223
224
        /*
225
         * Detect what action should be initiated
226
         *
227
         * Priorities:
228
         *  1. If action name in variables
229
         *  2. Action name in line with class name eg. MyApp\Index:test - test here is `action_test` alias
230
         *  3. Default action is index - action_index in result
231
         */
232
        $action = self::DEFAULT_ACTION;
233
        if (null !== $classAction) {
234
            $action = $classAction;
235
        }
236
        if (isset($variables['action'][0])) {
237
            $action = $variables['action'][0];
238
        }
239
240
        return 'action_' . $action;
241
    }
242
243
    /**
244
     * Check if method exist in required class
245
     *
246
     * @param   object $class
247
     * @param   string $action
248
     * @return  bool
249
     */
250
    private function methodCheck($class, string $action): bool
251
    {
252
        try {
253
            // If method not found in required class
254
            if (!\method_exists($class, $action)) {
255
                $className = \get_class($class);
256
                throw new Exception("Method \"$action\" is not found in \"$className\"");
257
            }
258
        } catch (Exception $e) {
259
            return false;
260
        }
261
        return true;
262
    }
263
264
    /**
265
     * Here we need to solve how to display the page, and if method is
266
     * not available need to show error
267
     *
268
     * @param   RouteInterface $route
269
     * @param   RequestInterface $request
270
     * @param   ResponseInterface $response
271
     * @param   bool $error
272
     * @return  StreamInterface
273
     */
274
    private function runClass(
275
        RouteInterface $route,
276
        RequestInterface $request,
277
        ResponseInterface $response,
278
        bool $error = false
279
    ) {
280
        $variables = $route->getVariables();
281
        $callback = $route->getCallback();
282
283
        // If extracted call back is string
284
        if (\is_string($callback)) {
285
286
            // Then class provided
287
            $class = new $callback();
288
            $action = $this->detectAction($callback, $variables);
289
290
            // If method is not found
291
            if (true !== $error && false === $this->methodCheck($class, $action)) {
292
                $router = $this->container('router');
293
                $routeError = $router->getError();
294
                return $this->runClass($routeError, $request, $response, true);
295
            }
296
297
            // Call required action, with request/response
298
            $class->$action($request, $response, $variables);
299
        } else {
300
            // Else simple callback
301
            $callback($request, $response, $variables);
302
        }
303
        return $response->getBody();
304
    }
305
306
    /**
307
     * Simple runner should parse query and make work on user's class
308
     *
309
     * @return  StreamInterface
310
     */
311
    public function run(): StreamInterface
312
    {
313
        // Extract some important objects
314
        $router = $this->container('router');
315
        $request = $this->container('request');
316
        $response = $this->container('response');
317
318
        // Get current matched route with and extract variables with callback
319
        $route = $router->getRoute();
320
321
        return $this->runClass($route, $request, $response);
322
    }
323
324
}
325