Completed
Push — master ( e7c634...2c7ea9 )
by Iman
02:03
created

BaseWidget::addDebugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
4
namespace Imanghafoori\Widgets;
5
6
abstract class BaseWidget
7
{
8
    protected $template = null;
9
    protected $minifyOutput = true;
10
    protected $cacheLifeTime = 'env_default';
11
    protected $contextAs = '$data';
12
    protected $presenter = 'default';
13
    protected $controller = null;
14
    protected $cacheTags = null;
15
    private $html;
16
    private $viewData;
17
18
    /**
19
     * BaseWidget constructor.
20
     */
21
    public function __construct()
22
    {
23
        $this->normalizeControllerMethod();
24
        $this->normalizePresenterName();
25
        $this->normalizeTemplateName();
26
        $this->normalizeContextAs();
27
        $this->normalizeCacheLifeTime();
28
        $this->normalizeCacheTags();
29
    }
30
31
    /**
32
     * Figures out which method should be called as the controller.
33
     * @return null
34
     */
35
    private function normalizeControllerMethod()
36
    {
37
        // If the user has explicitly declared controller class path on the sub-class
38
        if ($this->controller === null) {
39
            if (!method_exists($this, 'data')) {
40
                throw new \BadMethodCallException("'data' method not found on " . get_called_class());
41
            }
42
            // We decide to call data method on widget object.
43
            $this->controller = [$this, 'data'];
44
        } else {
45
            // If the user has specified the controller class path
46
            if (!class_exists($this->controller)) {
47
                throw new \InvalidArgumentException("Controller class: [{$this->controller}] not found.");
48
            }
49
50
            // we decide to call data method on that.
51
            $this->controller = ($this->controller) . '@data';
52
        }
53
    }
54
55
    /**
56
     * Figures out which method should be called as the presenter
57
     * @return null
58
     */
59
    private function normalizePresenterName()
60
    {
61
        if ($this->presenter === 'default') {
62
            $presenter = get_called_class() . 'Presenter';
63
64
            if (class_exists($presenter)) {
65
                $this->presenter = $presenter . '@presenter';
66
            } else {
67
                $this->presenter = null;
68
            }
69
        } else {
70
            if (class_exists($this->presenter) === false) {
71
                throw new \InvalidArgumentException("Presenter Class [{$this->presenter}] not found.");
72
            }
73
            $this->presenter = $this->presenter . '@present';
74
        }
75
76
    }
77
78
    /**
79
     * Figures out which template to render.
80
     * @return null
81
     */
82
    private function normalizeTemplateName()
83
    {
84
        // class name without namespace.
85
        $className = str_replace('App\\Widgets\\', '', get_called_class());
86
        // replace slashes with dots
87
        $className = str_replace(['\\', '/'], '.', $className);
88
89
        if ($this->template === null) {
90
            $this->template = 'Widgets::' . $className . 'View';
91
        }
92
93
        if (!view()->exists($this->template)) {
0 ignored issues
show
Bug introduced by
The method exists does only exist in Illuminate\Contracts\View\Factory, but not in Illuminate\View\View.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
94
            throw new \InvalidArgumentException("View file [{$className}View] not found by: '" . get_called_class() . " '");
95
        }
96
    }
97
98
    /**
99
     * Figures out what the variable name should be in view file.
100
     * @return null
101
     */
102
    private function normalizeContextAs()
103
    {
104
        // removes the $ sign.
105
        $this->contextAs = str_replace('$', '', (string)$this->contextAs);
106
    }
107
108
    /**
109
     * ّFigures out how long the cache life time should be.
110
     * @return null
111
     */
112
    private function normalizeCacheLifeTime()
113
    {
114
        if ($this->cacheLifeTime === 'env_default') {
115
            $this->cacheLifeTime = (int)(env('WIDGET_DEFAULT_CACHE_LIFETIME', 0));
0 ignored issues
show
Documentation Bug introduced by
The property $cacheLifeTime was declared of type string, but (int) env('WIDGET_DEFAULT_CACHE_LIFETIME', 0) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
116
        };
117
    }
118
119
    /**
120
     * ّFigures out what the cache tags should be.
121
     * @return null
122
     */
123
    private function normalizeCacheTags()
124
    {
125
        if ($this->cacheShouldBeTagged()) {
126
            if (is_string($this->cacheTags)) {
127
                $this->cacheTags = [$this->cacheTags];
128
            }
129
130
            if (!is_array($this->cacheTags)) {
131
                throw new \InvalidArgumentException('Cache Tags should be of type String or Array.');
132
            }
133
        }
134
    }
135
136
    /**
137
     * Determine whether cache tags should be applied or not
138
     * @return bool
139
     */
140
    private function cacheShouldBeTagged()
141
    {
142
        return !in_array(env('CACHE_DRIVER', 'file'), ['file', 'database']) and $this->cacheTags;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
143
    }
144
145
    /**
146
     * This method is called when you try to invoke the object like a function in blade files.
147
     * like this : {!! $myWidgetObj('param1') !!}
148
     * @param array $args
149
     * @return string
150
     */
151
    public function __invoke(...$args)
152
    {
153
        return $this->renderWidget(...$args);
154
    }
155
156
    /**
157
     * @param array $args
158
     * @return string
159
     */
160
    private function renderWidget(...$args)
161
    {
162
        try {
163
            $html = $this->generateHtml(...$args);
164
        } catch (\Exception $e) {
165
            return app()->make(ExceptionHandler::class)->render(app('request'), $e)->send();
166
        }
167
        return $html;
168
    }
169
170
    /**
171
     * It tries to get the html from cache if possible, otherwise generates it.
172
     * @param array ...$args
173
     * @return string
174
     */
175
    private function generateHtml(...$args)
176
    {
177
        // Everything inside this function is executed only when the cache is not available.
178
        $expensivePhpCode = function () use ($args) {
179
            $this->prepareDataForView($args);
180
181
            // render the template with the resulting data.
182
            return $this->renderTemplate();
183
        };
184
185
        $key = $this->makeCacheKey($args);
186
187
        // We first try to get the output from the cache before trying to run the expensive $expensivePhpCode...
188
        if ($this->widgetShouldUseCache()) {
189
            return $this->cacheResult($key, $expensivePhpCode);
190
        }
191
192
        return $expensivePhpCode();
193
    }
194
195
    /**
196
     * @param $args
197
     * @return null
198
     */
199
    private function prepareDataForView($args)
200
    {
201
        // Here we call the data method on the widget class.
202
        $viewData = \App::call($this->controller, $args);
203
204
        if (($this->presenter)) {
205
            // We make an object and call the `present` method on it.
206
            // Piping the data through the presenter before sending it to view.
207
            $viewData = \App::call($this->presenter, [$viewData]);
208
        }
209
210
        $this->viewData = $viewData;
211
    }
212
213
    private function renderTemplate()
214
    {
215
        // Here we render the view file to raw html.
216
        $this->html = view($this->template, [$this->contextAs => $this->viewData])->render();
0 ignored issues
show
Bug introduced by
The method render does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
217
218
        // We try to minify the html before storing it in cache to save space.
219
        if ($this->widgetShouldBeMinified()) {
220
            $this->minifyHtml();
221
        }
222
223
        // We add some HTML comments before and after the widget output
224
        // So then, we will be able to easily identify the widget in browser's developer tool.
225
        if (env('WIDGET_IDENTIFIER', true) and env('APP_ENV', 'production') === 'local') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
226
            $this->addIdentifierToHtml();
227
        }
228
229
        return $this->html;
230
    }
231
232
    /**
233
     * @return bool
234
     */
235
    private function widgetShouldBeMinified()
236
    {
237
        return env('WIDGET_MINIFICATION', false) or app()->environment('production');
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
238
    }
239
240
    /**
241
     * @return null
242
     */
243
    private function minifyHtml()
244
    {
245
        $replace = [
246
            '<!--(.*?)-->' => '', //remove comments
247
            "/<\?php/" => '<?php ',
248
            "/\n([\S])/" => '$1',
249
            "/\r/" => '', // remove carrage return
250
            "/\n/" => '', // remove new lines
251
            "/\t/" => '', // remove tab
252
            "/\s+/" => ' ', // remove spaces
253
        ];
254
255
        $this->html = preg_replace(array_keys($replace), array_values($replace), $this->html);
256
257
    }
258
259
    private function addIdentifierToHtml()
260
    {
261
        $this->addDebugInfo();
262
        $this->addHtmlComments();
263
    }
264
265
    /**
266
     * Generates a string of current cache configurations.
267
     * @return string
268
     */
269
    private function cacheState()
270
    {
271
        if ($this->widgetShouldUseCache()) {
272
            return " || cache: {$this->cacheLifeTime}(min)' ";
273
        }
274
        return " || cache : off";
275
    }
276
277
    /**
278
     * @return bool
279
     */
280
    private function widgetShouldUseCache()
281
    {
282
        /*
283
         * ================================== *
284
         |  The caching is turned off when:   |
285
         |  1- we are running tests           |
286
         |  2- have disabled it in .env file  |
287
         |  3- have set the time to 0 minutes |
288
         * ================================== *
289
        */
290
        return ((env('WIDGET_CACHE', false) !== false) and (!app()->environment('testing')) and ($this->cacheLifeTime !== 0));
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $this->cacheLifeTime (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
291
    }
292
293
    /**
294
     * @param $arg
295
     * @return string
296
     */
297
    private function makeCacheKey($arg)
298
    {
299
        return md5(json_encode($arg, JSON_FORCE_OBJECT) . $this->template . get_called_class());
300
    }
301
302
    private function cacheResult($key, $phpCode)
303
    {
304
        $cache = app()->make('cache');
305
306
        if ($this->cacheTags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cacheTags of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
307
            $cache = $cache->tags($this->cacheTags);
308
        }
309
310
        if ($this->cacheLifeTime > 0) {
311
            return $cache->remember($key, $this->cacheLifeTime, $phpCode);
312
        }
313
314
        if ($this->cacheLifeTime == 'forever' or $this->cacheLifeTime < 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
315
            return $cache->rememberForever($key, $phpCode);
316
        }
317
    }
318
319
    /**
320
     * This method is called when you try to print the object like an string in blade files.
321
     * like this : {!! $myWidgetObj !!}
322
     */
323
    public function __toString()
324
    {
325
        return $this->renderWidget();
326
    }
327
328
    private function addDebugInfo()
329
    {
330
        $this->html = "<div title='" . get_called_class() . "::class || template : {$this->template}" . $this->cacheState() . "' style='box-shadow: 0px 0px 15px 5px #00c62b inset'>" . $this->html . "</div>";
331
    }
332
333
    private function addHtmlComments()
334
    {
335
        $this->html = "<!-- '{$this->friendlyName}' Widget Start -->" . $this->html . "<!-- '{$this->friendlyName}' Widget End -->";
0 ignored issues
show
Bug introduced by
The property friendlyName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
336
    }
337
338
}
339