Controller::__get()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
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\Exception;
9
use MAKS\Velox\Backend\Event;
10
use MAKS\Velox\Backend\Config;
11
use MAKS\Velox\Backend\Router;
12
use MAKS\Velox\Backend\Globals;
13
use MAKS\Velox\Backend\Session;
14
use MAKS\Velox\Backend\Database;
15
use MAKS\Velox\Backend\Auth;
16
use MAKS\Velox\Backend\Model;
17
use MAKS\Velox\Frontend\Data;
18
use MAKS\Velox\Frontend\View;
19
use MAKS\Velox\Frontend\HTML;
20
use MAKS\Velox\Frontend\Path;
21
use MAKS\Velox\Helper\Dumper;
22
use MAKS\Velox\Helper\Misc;
23
24
/**
25
 * An abstract class that serves as a base Controller that can be extended to make handlers for application router.
26
 *
27
 * Example:
28
 * ```
29
 * // create a controller (alternatively, you can create it as a normal class in "/app/Controller/")
30
 * $additionalVars = [1, 2, 3];
31
 * $controller = new class($additionalVars) extends Controller {
32
 *      public function someAction(string $path, ?string $match, $previous) {
33
 *          $this->data->set('page.title', 'Some Page');
34
 *          $someVar = $this->config->get('filename.someVar');
35
 *          return $this->view->render('some-page', $this->vars);
36
 *      }
37
 * };
38
 *
39
 * // use the created action as a handler for a route
40
 * Router::handle('/some-route', [$controller, 'someAction'], ['GET', 'POST']);
41
 * ```
42
 *
43
 * @package Velox\Backend
44
 * @since 1.0.0
45
 * @api
46
 *
47
 * @property Event $event Instance of the `Event` class.
48
 * @property Config $config Instance of the `Config` class.
49
 * @property Router $router Instance of the `Router` class.
50
 * @property Globals $globals Instance of the `Globals` class.
51
 * @property Session $session Instance of the `Session` class.
52
 * @property Database $database Instance of the `Database` class.
53
 * @property Auth $auth Instance of the `Auth` class.
54
 * @property Data $data Instance of the `Data` class.
55
 * @property View $view Instance of the `View` class.
56
 * @property HTML $html Instance of the `HTML` class.
57
 * @property Path $path Instance of the `Path` class.
58
 * @property Dumper $dumper Instance of the `Dumper` class.
59
 * @property Misc $misc Instance of the `Misc` class.
60
 */
61
abstract class Controller
62
{
63
    /**
64
     * This event will be dispatched when a controller (or a subclass) is constructed.
65
     * This event will not be passed any arguments, but its listener callback will be bound to the object (the controller class).
66
     *
67
     * @var string
68
     */
69
    public const ON_CONSTRUCT = 'controller.on.construct';
70
71
72
    /**
73
     * Preconfigured CRUD routes.
74
     *
75
     * @since 1.3.0
76
     */
77
    private array $crudRoutes = [
78
        'index' => [
79
            'expression' => '/{controller}',
80
            'method' => 'GET',
81
        ],
82
        'create' => [
83
            'expression' => '/{controller}/create',
84
            'method' => 'GET',
85
        ],
86
        'store' => [
87
            'expression' => '/{controller}',
88
            'method' => 'POST',
89
        ],
90
        'show' => [
91
            'expression' => '/{controller}/([1-9][0-9]*)',
92
            'method' => 'GET',
93
        ],
94
        'edit' => [
95
            'expression' => '/{controller}/([1-9][0-9]*)/edit',
96
            'method' => 'GET',
97
        ],
98
        'update' => [
99
            'expression' => '/{controller}/([1-9][0-9]*)',
100
            'method' => ['PUT', 'PATCH'],
101
        ],
102
        'destroy' => [
103
            'expression' => '/{controller}/([1-9][0-9]*)',
104
            'method' => 'DELETE',
105
        ],
106
    ];
107
108
    /**
109
     * The passed variables array to the Controller.
110
     */
111
    protected array $vars;
112
113
    protected ?Model $model;
114
115
116
    /**
117
     * Class constructor.
118
     *
119
     * @param array $vars [optional] Additional variables to pass to the controller.
120
     */
121 2
    public function __construct(array $vars = [])
122
    {
123 2
        $this->vars  = $vars;
124 2
        $this->model = null;
125
126 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...
127 2
            $this->doAssociateModel();
128
        }
129
130 2
        if ($this->registerRoutes()) {
131 2
            $this->doRegisterRoutes();
132
        }
133
134 2
        Event::dispatch(self::ON_CONSTRUCT, null, $this);
135
    }
136
137 2
    public function __get(string $property)
138
    {
139 2
        if (isset(App::instance()->{$property})) {
140 2
            return App::instance()->{$property};
141
        }
142
143 1
        Exception::throw(
144
            'UndefinedPropertyException:OutOfBoundsException',
145 1
            sprintf('Call to undefined property %s::$%s', static::class, $property),
146
        );
147
    }
148
149 1
    public function __isset(string $property)
150
    {
151 1
        return isset(App::instance()->{$property});
152
    }
153
154
155
    /**
156
     * Controls which model should be used by current controller.
157
     *
158
     * This method should return a concrete class FQN of a model that extends `Model::class`.
159
     *
160
     * This method returns `null` by default.
161
     *
162
     * NOTE: If the model class does not exist, the controller will ignore it silently.
163
     *
164
     * @return string
165
     *
166
     * @since 1.3.0
167
     */
168
    protected function associateModel(): ?string
169
    {
170
        return null; // @codeCoverageIgnore
171
    }
172
173
    /**
174
     * Whether or not to automatically register controller routes.
175
     *
176
     * NOTE: The controller class has to be instantiated at least once for this to work.
177
     *
178
     * Only public methods suffixed with the word `Action` or `Middleware` will be registered.
179
     * The suffix will determine the route type (`*Action` => `handler`, `*Middleware` => `middleware`).
180
     * The route will look like `/controller-name/method-name` (names will be converted to slugs).
181
     * The method will be `GET` by default. See also `self::$crudRoutes`.
182
     * You can use the `@route` annotation to overrides the default Route and Method.
183
     * The `@route` annotation can be used in DocBlock on a class method with the following syntax:
184
     * - Pattern: `@route("<path>", {<http-verb>, ...})`
185
     * - Example: `@route("/some-route", {GET, POST})`
186
     *
187
     * This method returns `false` by default.
188
     *
189
     * @return bool
190
     *
191
     * @since 1.3.0
192
     */
193
    protected function registerRoutes(): bool
194
    {
195
        return false; // @codeCoverageIgnore
196
    }
197
198
    /**
199
     * Associates a model class to the controller.
200
     *
201
     * @return void
202
     *
203
     * @since 1.3.0
204
     */
205 2
    private function doAssociateModel(): void
206
    {
207 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...
208
        // to prevent \ReflectionClass from throwing an exception
209 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

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