Completed
Push — master ( ddb62a...66eba0 )
by Mr
02:04
created

App::extractClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
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
     * Parce 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
        /*
256
         * Detect what action should be initiated
257
         *
258
         * Priorities:
259
         */
260
261
        // 3. Default action is index - action_index in result
262
        $action = self::DEFAULT_ACTION;
263
264
        // 2. If action name in variables
265
        if (isset($variables['action'][0])) {
266
            $action = $variables['action'][0];
267
        }
268
269
        // 1. Action name in line with class name eg. MyApp\Index:test - test
270
        //    here is `action_test` alias
271
        $classAction = $this->extractActionFromClass($className);
272
        if (null !== $classAction) {
273
            $action = $classAction;
274
        }
275
276
        return 'action_' . $action;
277
    }
278
279
    /**
280
     * Check if method exist in required class
281
     *
282
     * @param   object $class
283
     * @param   string $action
284
     * @return  bool
285
     */
286
    private function methodCheck($class, string $action): bool
287
    {
288
        try {
289
            // If method not found in required class
290
            if (!\method_exists($class, $action)) {
291
                $className = \get_class($class);
292
                throw new Exception("Method \"$action\" is not found in \"$className\"");
293
            }
294
        } catch (Exception $e) {
295
            return false;
296
        }
297
        return true;
298
    }
299
300
    /**
301
     * Here we need to solve how to display the page, and if method is
302
     * not available need to show error
303
     *
304
     * @param   RouteInterface $route
305
     * @param   RequestInterface $request
306
     * @param   ResponseInterface $response
307
     * @param   bool $error
308
     * @return  StreamInterface
309
     */
310
    private function exec(
311
        RouteInterface $route,
312
        RequestInterface $request,
313
        ResponseInterface $response,
314
        bool $error = false
315
    ) {
316
        $variables = $route->getVariables();
317
        $callback = $route->getCallback();
318
319
        // If extracted call back is string
320
        if (\is_string($callback)) {
321
322
            $className = $this->extractClass($callback);
323
324
            // Then class provided
325
            $class = new $className();
326
            $action = $this->detectAction($callback, $variables);
327
328
            // If method is not found
329
            if (true !== $error && false === $this->methodCheck($class, $action)) {
330
                $router = $this->container('router');
331
                $routeError = $router->getError();
332
                return $this->exec($routeError, $request, $response, true);
333
            }
334
335
            // Call required action, with request/response
336
            $class->$action($request, $response, $variables);
337
        } else {
338
            // Else simple callback
339
            $callback($request, $response, $variables);
340
        }
341
        return $response->getBody();
342
    }
343
344
    /**
345
     * Simple runner should parse query and make work on user's class
346
     *
347
     * @return  StreamInterface
348
     */
349
    public function run(): StreamInterface
350
    {
351
        // Extract some important objects
352
        $router = $this->container('router');
353
        $request = $this->container('request');
354
        $response = $this->container('response');
355
356
        // Get current matched route with and extract variables with callback
357
        $route = $router->getRoute();
358
359
        return $this->exec($route, $request, $response);
360
    }
361
362
}
363