Passed
Push — master ( af13cf...4b678b )
by Mr
02:19
created

App::detectAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 13
rs 9.4285
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\Response;
11
12
use DrMVC\Controllers\Error;
13
use DrMVC\Router\Router;
14
use DrMVC\Router\RouteInterface;
15
use DrMVC\Router\MethodsInterface;
16
use DrMVC\Config\ConfigInterface;
17
18
/**
19
 * Main class of DrMVC application
20
 * @package DrMVC\Framework
21
 * @since   3.0
22
 *
23
 * @method App options(string $pattern, callable $callable): App
24
 * @method App get(string $pattern, callable $callable): App
25
 * @method App head(string $pattern, callable $callable): App
26
 * @method App post(string $pattern, callable $callable): App
27
 * @method App put(string $pattern, callable $callable): App
28
 * @method App delete(string $pattern, callable $callable): App
29
 * @method App trace(string $pattern, callable $callable): App
30
 * @method App connect(string $pattern, callable $callable): App
31
 */
32
class App implements AppInterface
33
{
34
35
    /**
36
     * Default action if method is not set
37
     */
38
    const DEFAULT_ACTION = 'index';
39
40
    /**
41
     * @var ContainersInterface
42
     */
43
    private $_containers;
44
45
    /**
46
     * App constructor.
47
     * @param ConfigInterface $config
48
     */
49
    public function __construct(ConfigInterface $config)
50
    {
51
        // Initiate PSR-11 containers
52
        $this->initContainers();
53
54
        // Save configuration
55
        $this->initConfig($config);
56
57
        // Initiate router
58
        $this
59
            ->initRequest()
60
            ->initResponse()
61
            ->initRouter();
62
    }
63
64
    /**
65
     * Initialize containers object
66
     *
67
     * @return  App
68
     */
69
    private function initContainers(): App
70
    {
71
        if (null === $this->_containers) {
72
            $this->_containers = new Containers();
73
        }
74
        return $this;
75
    }
76
77
    /**
78
     * Put config object into the containers class
79
     *
80
     * @param   ConfigInterface $config
81
     * @return  App
82
     */
83
    private function initConfig(ConfigInterface $config): App
84
    {
85
        $this->containers()->set('config', $config);
86
        return $this;
87
    }
88
89
    /**
90
     * Initiate PSR-7 request object
91
     *
92
     * @return  App
93
     */
94
    private function initRequest(): App
95
    {
96
        try {
97
            $request = ServerRequestFactory::fromGlobals();
98
            $this->containers()->set('request', $request);
99
        } catch (\InvalidArgumentException $e) {
100
            new Exception($e);
101
        }
102
        return $this;
103
    }
104
105
    /**
106
     * Initiate PSR-7 response object
107
     *
108
     * @return  App
109
     */
110
    private function initResponse(): App
111
    {
112
        try {
113
            $response = new Response();
114
            $this->containers()->set('response', $response);
115
        } catch (\InvalidArgumentException $e) {
116
            new Exception($e);
117
        }
118
        return $this;
119
    }
120
121
    /**
122
     * Put route into the container of classes
123
     *
124
     * @return  App
125
     */
126
    private function initRouter(): App
127
    {
128
        $request = $this->container('request');
129
        $response = $this->container('response');
130
        $router = new Router($request, $response);
131
        $router->error(Error::class);
132
133
        $this->containers()->set('router', $router);
134
        return $this;
135
    }
136
137
    /**
138
     * Get custom container by name
139
     *
140
     * @param   string $name
141
     * @return  mixed
142
     */
143
    public function container(string $name)
144
    {
145
        return $this->_containers->get($name);
146
    }
147
148
    /**
149
     * Get all available containers
150
     *
151
     * @return  ContainersInterface
152
     */
153
    public function containers(): ContainersInterface
154
    {
155
        return $this->_containers;
156
    }
157
158
    /**
159
     * Magic method for work with calls
160
     *
161
     * @param   string $method
162
     * @param   array $args
163
     * @return  MethodsInterface
164
     */
165
    public function __call(string $method, array $args): MethodsInterface
166
    {
167
        if (\in_array($method, Router::METHODS, false)) {
168
            $this->container('router')->set([$method], $args);
169
        }
170
        return $this;
171
    }
172
173
    /**
174
     * If any route is provided
175
     *
176
     * @param   string $pattern
177
     * @param   callable|string $callable
178
     * @return  MethodsInterface
179
     */
180
    public function any(string $pattern, $callable): MethodsInterface
181
    {
182
        $this->container('router')->any($pattern, $callable);
183
        return $this;
184
    }
185
186
    /**
187
     * Set the error callback of class
188
     *
189
     * @param   callable|string $callable
190
     * @return  MethodsInterface
191
     */
192
    public function error($callable): MethodsInterface
193
    {
194
        $this->container('router')->error($callable);
195
        return $this;
196
    }
197
198
    /**
199
     * Few methods provided
200
     *
201
     * @param   array $methods
202
     * @param   string $pattern
203
     * @param   callable|string $callable
204
     * @return  MethodsInterface
205
     */
206
    public function map(array $methods, string $pattern, $callable): MethodsInterface
207
    {
208
        $this->container('router')->map($methods, $pattern, $callable);
209
        return $this;
210
    }
211
212
    /**
213
     * Here we need parse line of class and extract action name after last ":" symbol
214
     *
215
     * @param   string $className
216
     * @return  string|null
217
     */
218
    private function extractActionFromClass(string $className)
219
    {
220
        // If class contain method name
221
        if (strpos($className, ':') !== false) {
222
            $classArray = explode(':', $className);
223
            $classAction = end($classArray);
224
        } else {
225
            $classAction = null;
226
        }
227
        return $classAction;
228
    }
229
230
    /**
231
     * Parse line of class, end return only class name (without possible action)
232
     *
233
     * @param   string $className
234
     * @return  string
235
     */
236
    private function extractClass(string $className): string
237
    {
238
        // If class name contain ":" symbol
239
        if (strpos($className, ':') !== false) {
240
            $classArray = explode(':', $className);
241
            $className = $classArray[0];
242
        }
243
        return $className;
244
    }
245
246
    /**
247
     * Detect action by string name, variable or use default
248
     *
249
     * @param   string $className - eg. MyApp\Index:test
250
     * @param   array $variables
251
     * @return  string
252
     */
253
    private function detectAction(string $className, array $variables = []): string
254
    {
255
        $action =
256
            // 1. Action name in line with class name eg. MyApp\Index:test - alias for `action_test`
257
            $this->extractActionFromClass($className)
258
            ?? (
259
                // 2. If action name in variables (we need first item)
260
                $variables['action'][0]
261
                // 3. Default action is index
262
                ?? self::DEFAULT_ACTION
263
            );
264
265
        return 'action_' . $action;
266
    }
267
268
    /**
269
     * Check if method exist in required class
270
     *
271
     * @param   object $class
272
     * @param   string $action
273
     * @return  bool
274
     */
275
    private function methodCheck($class, string $action): bool
276
    {
277
        try {
278
            // If method not found in required class
279
            if (!\method_exists($class, $action)) {
280
                $className = \get_class($class);
281
                throw new Exception("Method \"$action\" is not found in \"$className\"");
282
            }
283
        } catch (Exception $e) {
284
            return false;
285
        }
286
        return true;
287
    }
288
289
    /**
290
     * Here we need to solve how to display the page, and if method is
291
     * not available need to show error
292
     *
293
     * @param   RouteInterface $route
294
     * @param   RequestInterface $request
295
     * @param   ResponseInterface $response
296
     * @param   bool $error
297
     * @return  StreamInterface
298
     */
299
    private function exec(
300
        RouteInterface $route,
301
        RequestInterface $request,
302
        ResponseInterface $response,
303
        bool $error = false
304
    ) {
305
        $variables = $route->getVariables();
306
        $callback = $route->getCallback();
307
308
        // If extracted call back is string
309
        if (\is_string($callback)) {
310
311
            $className = $this->extractClass($callback);
312
313
            // Then class provided
314
            $class = new $className();
315
            $action = $this->detectAction($callback, $variables);
316
317
            // If method is not found in class and error is not triggered, then trigger error
318
            if (true !== $error && false === $this->methodCheck($class, $action)) {
319
                $router = $this->container('router');
320
                $routeError = $router->getError();
321
                return $this->exec($routeError, $request, $response, true);
322
            }
323
324
            // Call required action, with request/response
325
            $class->$action($request, $response, $variables);
326
        } else {
327
            // Else simple callback
328
            $callback($request, $response, $variables);
329
        }
330
        return $response->getBody();
331
    }
332
333
    /**
334
     * Simple runner should parse query and make work on user's class
335
     *
336
     * @return  StreamInterface
337
     */
338
    public function run(): StreamInterface
339
    {
340
        // Extract some important objects
341
        $router = $this->container('router');
342
        $request = $this->container('request');
343
        $response = $this->container('response');
344
345
        // Get current matched route with and extract variables with callback
346
        $route = $router->getRoute();
347
348
        return $this->exec($route, $request, $response);
349
    }
350
351
}
352