Passed
Push — master ( 57cfb9...a2aa4e )
by Marwan
10:09
created

Controller::__isset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
cc 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MAKS\Velox\Backend;
6
7
use MAKS\Velox\App;
8
use MAKS\Velox\Backend\Event;
9
use MAKS\Velox\Backend\Config;
10
use MAKS\Velox\Backend\Router;
11
use MAKS\Velox\Backend\Globals;
12
use MAKS\Velox\Backend\Session;
13
use MAKS\Velox\Backend\Database;
14
use MAKS\Velox\Backend\Auth;
15
use MAKS\Velox\Backend\Model;
16
use MAKS\Velox\Frontend\Data;
17
use MAKS\Velox\Frontend\View;
18
use MAKS\Velox\Frontend\HTML;
19
use MAKS\Velox\Frontend\Path;
20
use MAKS\Velox\Helper\Dumper;
21
use MAKS\Velox\Helper\Misc;
22
23
/**
24
 * An abstract class that serves as a base Controller that can be extended to make handlers for application router.
25
 *
26
 * Example:
27
 * ```
28
 * // create a controller (alternatively, you can create it as a normal class in "/app/Controller/")
29
 * $additionalVars = [1, 2, 3];
30
 * $controller = new class($additionalVars) extends Controller {
31
 *      public function someAction(string $path, ?string $match, $previous) {
32
 *          $this->data->set('page.title', 'Some Page');
33
 *          $someVar = $this->config->get('filename.someVar');
34
 *          return $this->view->render('some-page', $this->vars);
35
 *      }
36
 * };
37
 *
38
 * // use the created action as a handler for a route
39
 * Router::handle('/some-route', [$controller, 'someAction'], ['GET', 'POST']);
40
 * ```
41
 *
42
 * @property Event $event Instance of the `Event` class.
43
 * @property Config $config Instance of the `Config` class.
44
 * @property Router $router Instance of the `Router` class.
45
 * @property Globals $globals Instance of the `Globals` class.
46
 * @property Session $session Instance of the `Session` class.
47
 * @property Database $database Instance of the `Database` class.
48
 * @property Auth $auth Instance of the `Auth` class.
49
 * @property Data $data Instance of the `Data` class.
50
 * @property View $view Instance of the `View` class.
51
 * @property HTML $html Instance of the `HTML` class.
52
 * @property Path $path Instance of the `Path` class.
53
 * @property Dumper $dumper Instance of the `Dumper` class.
54
 * @property Misc $misc Instance of the `Misc` class.
55
 *
56
 * @since 1.0.0
57
 * @api
58
 */
59
abstract class Controller
60
{
61
    /**
62
     * This event will be dispatched when a controller (or a subclass) is constructed.
63
     * This event will not be passed any arguments, but its listener callback will be bound to the object (the controller class).
64
     *
65
     * @var string
66
     */
67
    public const ON_CONSTRUCT = 'controller.on.construct';
68
69
70
    /**
71
     * Preconfigured CRUD routes.
72
     *
73
     * @since 1.3.0
74
     */
75
    private array $crudRoutes = [
76
        'index' => [
77
            'expression' => '/{controller}',
78
            'method' => 'GET',
79
        ],
80
        'create' => [
81
            'expression' => '/{controller}/create',
82
            'method' => 'GET',
83
        ],
84
        'store' => [
85
            'expression' => '/{controller}',
86
            'method' => 'POST',
87
        ],
88
        'show' => [
89
            'expression' => '/{controller}/([0-9]+)',
90
            'method' => 'GET',
91
        ],
92
        'edit' => [
93
            'expression' => '/{controller}/([0-9]+)/edit',
94
            'method' => 'GET',
95
        ],
96
        'update' => [
97
            'expression' => '/{controller}/([0-9]+)',
98
            'method' => ['PUT', 'PATCH'],
99
        ],
100
        'destroy' => [
101
            'expression' => '/{controller}/([0-9]+)',
102
            'method' => 'DELETE',
103
        ],
104
    ];
105
106
    /**
107
     * The passed variables array to the Controller.
108
     */
109
    protected array $vars;
110
111
    protected ?Model $model;
112
113
114
    /**
115
     * Class constructor.
116
     *
117
     * @param array $vars [optional] Additional variables to pass to the controller.
118
     */
119 2
    public function __construct(array $vars = [])
120
    {
121 2
        $this->vars  = $vars;
122 2
        $this->model = null;
123
124 2
        if ($this->associateModel()) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->associateModel() targeting MAKS\Velox\Backend\Controller::associateModel() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
125 2
            $this->doAssociateModel();
126
        }
127
128 2
        if ($this->registerRoutes()) {
129 2
            $this->doRegisterRoutes();
130
        }
131
132 2
        Event::dispatch(self::ON_CONSTRUCT, null, $this);
133 2
    }
134
135 2
    public function __get(string $property)
136
    {
137 2
        if (isset(App::instance()->{$property})) {
138 2
            return App::instance()->{$property};
139
        }
140
141 1
        $class = static::class;
142
143 1
        throw new \Exception("Call to undefined property {$class}::${$property}");
144
    }
145
146 1
    public function __isset(string $property)
147
    {
148 1
        return isset(App::instance()->{$property});
149
    }
150
151
152
    /**
153
     * Controls which model should be used by current controller.
154
     *
155
     * This method should return a concrete class FQN of a model that extends `Model::class`.
156
     *
157
     * This method returns `null` by default.
158
     *
159
     * NOTE: If the model class does not exist, the controller will ignore it silently.
160
     *
161
     * @return string
162
     *
163
     * @since 1.3.0
164
     */
165
    protected function associateModel(): ?string
166
    {
167
        return null; // @codeCoverageIgnore
168
    }
169
170
    /**
171
     * Whether or not to automatically register controller routes.
172
     *
173
     * NOTE: The controller class has to be instantiated at least once for this to work.
174
     *
175
     * Only public methods suffixed with the word `Action` or `Middleware` will be registered.
176
     * The suffix will determine the route type (`*Action` => `handler`, `*Middleware` => `middleware`).
177
     * The route will look like `/controller-name/method-name` (names will be converted to slugs).
178
     * The method will be `GET` by default. See also `self::$crudRoutes`.
179
     * You can use the `@route` annotation to overrides the default Route and Method.
180
     * The `@route` annotation can be used in DocBlock on a class method with the following syntax:
181
     * - Pattern: `@route("<path>", {<http-verb>, ...})`
182
     * - Example: `@route("/some-route", {GET, POST})`
183
     *
184
     * This method returns `false` by default.
185
     *
186
     * @return bool
187
     *
188
     * @since 1.3.0
189
     */
190
    protected function registerRoutes(): bool
191
    {
192
        return false; // @codeCoverageIgnore
193
    }
194
195
    /**
196
     * Associates a model class to the controller.
197
     *
198
     * @return void
199
     *
200
     * @since 1.3.0
201
     */
202 2
    private function doAssociateModel(): void
203
    {
204 2
        $model = $this->associateModel();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $model is correct as $this->associateModel() targeting MAKS\Velox\Backend\Controller::associateModel() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
205
        // to prevent \ReflectionClass from throwing an exception
206 2
        $model = class_exists($model) ? $model : Model::class;
0 ignored issues
show
Bug introduced by
$model of type null is incompatible with the type string expected by parameter $class of class_exists(). ( Ignorable by Annotation )

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

206
        $model = class_exists(/** @scrutinizer ignore-type */ $model) ? $model : Model::class;
Loading history...
207
208 2
        $reflection = new \ReflectionClass($model);
209
210 2
        if ($reflection->isSubclassOf(Model::class) && !$reflection->isAbstract()) {
211 2
            $this->model = $reflection->newInstance();
212
        }
213 2
    }
214
215
    /**
216
     * Registers all public methods which are suffixed with `Action` as routes.
217
     *
218
     * @return void
219
     *
220
     * @since 1.3.0
221
     */
222 2
    private function doRegisterRoutes(): void
223
    {
224 2
        $class   = new \ReflectionClass($this);
225 2
        $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
226
227 2
        foreach ($methods as $method) {
228 2
            $className  = $class->getShortName();
229 2
            $methodName = $method->getName();
230 2
            $docBlock   = $method->getDocComment() ?: '';
231
232
            if (
233 2
                $method->isAbstract() ||
234 2
                $method->isStatic() ||
235 2
                preg_match('/(Action|Middleware)$/', $methodName) === 0
236
            ) {
237 2
                continue;
238
            }
239
240 2
            $controller  = Misc::transform(str_replace('Controller', '', $className), 'kebab', 'slug');
241 2
            $handler     = Misc::transform(str_replace(['Action', 'Middleware'], '', $methodName), 'kebab', 'slug');
242
243 2
            $routes = $this->crudRoutes;
244
245 2
            if (!in_array($handler, array_keys($routes))) {
246 2
                Misc::setArrayValueByKey(
247 2
                    $routes,
248 2
                    $handler . '.expression',
249 2
                    sprintf('/%s/%s', $controller, $handler)
250
                );
251
            }
252
253 2
            if (preg_match('/(@route[ ]*\(["\'](.+)["\']([ ,]*\{(.+)\})?\))/', $docBlock, $matches)) {
254 2
                $routeExpression = $matches[2] ?? '';
255 2
                $routeMethod     = $matches[4] ?? '';
256
257 2
                $routeMethod = array_filter(array_map('trim', explode(',', $routeMethod)));
258 2
                $routes[$handler] = [
259 2
                    'expression' => $routeExpression,
260 2
                    'method'     => $routeMethod,
261
                ];
262
            }
263
264 2
            $function   = preg_match('/(Middleware)$/', $methodName) ? 'middleware' : 'handle';
265 2
            $expression = Misc::interpolate($routes[$handler]['expression'], ['controller' => $controller]);
266 2
            $method     = $routes[$handler]['method'] ?? 'GET';
267
268 2
            $this->router->{$function}($expression, [$this, $methodName], $method);
269
        }
270 2
    }
271
}
272