Completed
Push — master ( d28ef0...82717b )
by Mr
02:59 queued 16s
created

App::extractActionFromClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 10
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\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);
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);
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);
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);
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
     * Here we need parse line of class and extract action name after last ":" symbol
209
     *
210
     * @param   string $className
211
     * @return  string
212
     */
213
    private function extractActionFromClass(string $className): string
214
    {
215
        // If class contain method name
216
        if (strpos($className, ':') !== false) {
217
            $classArray = explode(':', $className);
218
            $classAction = end($classArray);
219
        } else {
220
            $classAction = null;
221
        }
222
        return $classAction;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $classAction could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
223
    }
224
225
    /**
226
     * Detect action by string name, variable or use default
227
     *
228
     * @param   string $className - eg. MyApp\Index:test
229
     * @param   array $variables
230
     * @return  string
231
     */
232
    private function detectAction(string $className, array $variables = []): string
233
    {
234
        /*
235
         * Detect what action should be initiated
236
         *
237
         * Priorities:
238
         *  1. If action name in variables
239
         *  2. Action name in line with class name eg. MyApp\Index:test - test here is `action_test` alias
240
         *  3. Default action is index - action_index in result
241
         */
242
        $action = self::DEFAULT_ACTION;
243
244
        $classAction = $this->extractActionFromClass($className);
245
        if (null !== $classAction) {
0 ignored issues
show
introduced by
The condition null !== $classAction is always true.
Loading history...
246
            $action = $classAction;
247
        }
248
249
        if (isset($variables['action'][0])) {
250
            $action = $variables['action'][0];
251
        }
252
253
        return 'action_' . $action;
254
    }
255
256
    /**
257
     * Check if method exist in required class
258
     *
259
     * @param   object $class
260
     * @param   string $action
261
     * @return  bool
262
     */
263
    private function methodCheck($class, string $action): bool
264
    {
265
        try {
266
            // If method not found in required class
267
            if (!\method_exists($class, $action)) {
268
                $className = \get_class($class);
269
                throw new Exception("Method \"$action\" is not found in \"$className\"");
270
            }
271
        } catch (Exception $e) {
272
            return false;
273
        }
274
        return true;
275
    }
276
277
    /**
278
     * Here we need to solve how to display the page, and if method is
279
     * not available need to show error
280
     *
281
     * @param   RouteInterface $route
282
     * @param   RequestInterface $request
283
     * @param   ResponseInterface $response
284
     * @param   bool $error
285
     * @return  StreamInterface
286
     */
287
    private function runClass(
288
        RouteInterface $route,
289
        RequestInterface $request,
290
        ResponseInterface $response,
291
        bool $error = false
292
    ) {
293
        $variables = $route->getVariables();
294
        $callback = $route->getCallback();
295
296
        // If extracted call back is string
297
        if (\is_string($callback)) {
298
299
            // Then class provided
300
            $class = new $callback();
301
            $action = $this->detectAction($callback, $variables);
302
303
            // If method is not found
304
            if (true !== $error && false === $this->methodCheck($class, $action)) {
305
                $router = $this->container('router');
306
                $routeError = $router->getError();
307
                return $this->runClass($routeError, $request, $response, true);
308
            }
309
310
            // Call required action, with request/response
311
            $class->$action($request, $response, $variables);
312
        } else {
313
            // Else simple callback
314
            $callback($request, $response, $variables);
315
        }
316
        return $response->getBody();
317
    }
318
319
    /**
320
     * Simple runner should parse query and make work on user's class
321
     *
322
     * @return  StreamInterface
323
     */
324
    public function run(): StreamInterface
325
    {
326
        // Extract some important objects
327
        $router = $this->container('router');
328
        $request = $this->container('request');
329
        $response = $this->container('response');
330
331
        // Get current matched route with and extract variables with callback
332
        $route = $router->getRoute();
333
334
        return $this->runClass($route, $request, $response);
335
    }
336
337
}
338