Completed
Push — develop ( 3d6b5a...688bc1 )
by Mohamed
07:41
created

Issue::rules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 9
ccs 3
cts 3
cp 1
rs 9.6666
cc 1
eloc 5
nc 1
nop 0
crap 1
1
<?php
2
3
/*
4
 * This file is part of the Tinyissue package.
5
 *
6
 * (c) Mohamed Alsharaf <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Tinyissue\Form;
13
14
use Tinyissue\Model;
15
16
/**
17
 * Issue is a class to defines fields & rules for add/edit issue form.
18
 *
19
 * @author Mohamed Alsharaf <[email protected]>
20
 */
21
class Issue extends FormAbstract
22
{
23
    /**
24
     * An instance of project model.
25
     *
26
     * @var Model\Project
27
     */
28
    protected $project;
29
30
    /**
31
     * Collection of all tags.
32
     *
33
     * @var \Illuminate\Database\Eloquent\Collection
34
     */
35
    protected $tags = null;
36
37
    /**
38
     * @param string $type
39
     *
40
     * @return \Illuminate\Database\Eloquent\Collection|null
41
     */
42 7 View Code Duplication
    protected function getTags($type = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
43
    {
44 7
        if ($this->tags === null) {
45 7
            $this->tags = (new Model\Tag())->getGroupTags();
46
        }
47
48 7
        if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
49 7
            return $this->tags->where('name', $type)->first()->tags;
50
        }
51
52
        return $this->tags;
53
    }
54
55
    /**
56
     * Get issue tag for specific type/group.
57
     *
58
     * @param string $type
59
     *
60
     * @return int
61
     */
62 7
    protected function getIssueTag($type)
63
    {
64 7
        if ($this->isEditing()) {
65 2
            $groupId     = $this->getTags($type)->first()->parent_id;
66 2
            $selectedTag = $this->getModel()->tags->where('parent_id', $groupId);
67
68 2
            if ($selectedTag->count() > 0) {
69
                return $selectedTag->last();
70
            }
71
        }
72
73 7
        return new Model\Tag();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Tinyissue\Model\Tag(); (Tinyissue\Model\Tag) is incompatible with the return type documented by Tinyissue\Form\Issue::getIssueTag of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
74
    }
75
76
    /**
77
     * @param array $params
78
     *
79
     * @return void
80
     */
81 6
    public function setup(array $params)
82
    {
83 6
        $this->project = $params['project'];
84 6
        if (!empty($params['issue'])) {
85 2
            $this->editingModel($params['issue']);
86
        }
87 6
    }
88
89
    /**
90
     * @return array
91
     */
92 6
    public function actions()
93
    {
94
        $actions = [
95 6
            'submit' => $this->isEditing() ? 'update_issue' : 'create_issue',
96
        ];
97
98 6
        if ($this->isEditing() && auth()->user(Model\Permission::PERM_ISSUE_MODIFY)) {
0 ignored issues
show
Unused Code introduced by
The call to Guard::user() has too many arguments starting with \Tinyissue\Model\Permission::PERM_ISSUE_MODIFY.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
99 2
            $actions['delete'] = [
100 2
                'type'         => 'danger_submit',
101 2
                'label'        => trans('tinyissue.delete_something', ['name' => '#' . $this->getModel()->id]),
102 2
                'class'        => 'close-issue',
103 2
                'name'         => 'delete-issue',
104 2
                'data-message' => trans('tinyissue.delete_issue_confirm'),
105
            ];
106
        }
107
108 6
        return $actions;
109
    }
110
111
    /**
112
     * @return array
113
     */
114 6
    public function fields()
115
    {
116 6
        $issueModify = \Auth::user()->permission('issue-modify');
117
118 6
        $fields = $this->fieldTitle();
119 6
        $fields += $this->fieldBody();
120 6
        $fields += $this->fieldTypeTags();
121
122
        // Only on creating new issue
123 6
        if (!$this->isEditing()) {
124 5
            $fields += $this->fieldUpload();
125
        }
126
127
        // Show fields for users with issue modify permission
128 6
        if ($issueModify) {
129 6
            $fields += $this->issueModifyFields();
130
        }
131
132 6
        return $fields;
133
    }
134
135
    /**
136
     * Return a list of fields for users with issue modify permission.
137
     *
138
     * @return array
139
     */
140 6
    protected function issueModifyFields()
141
    {
142 6
        $fields = [];
143
144 6
        $fields['internal_status'] = [
145
            'type' => 'legend',
146
        ];
147
148
        // Status tags
149 6
        $fields += $this->fieldStatusTags();
150
151
        // Assign users
152 6
        $fields += $this->fieldAssignedTo();
153
154
        // Quotes
155 6
        $fields += $this->fieldTimeQuote();
156
157
        // Resolution tags
158 6
        $fields += $this->fieldResolutionTags();
159
160 6
        return $fields;
161
    }
162
163
    /**
164
     * Returns title field.
165
     *
166
     * @return array
167
     */
168 7
    protected function fieldTitle()
169
    {
170
        return [
171
            'title' => [
172
                'type'  => 'text',
173
                'label' => 'title',
174 7
            ],
175
        ];
176
    }
177
178
    /**
179
     * Returns body field.
180
     *
181
     * @return array
182
     */
183 7
    protected function fieldBody()
184
    {
185
        return [
186
            'body' => [
187
                'type'  => 'textarea',
188
                'label' => 'issue',
189 7
            ],
190
        ];
191
    }
192
193
    /**
194
     * Returns status tag field.
195
     *
196
     * @return array
197
     */
198 6
    protected function fieldStatusTags()
199
    {
200 6
        $currentTag = $this->getIssueTag('status');
201
202 6
        if ($currentTag && !$currentTag->canView()) {
0 ignored issues
show
Bug introduced by
The method canView cannot be called on $currentTag (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
203
            $tags = [$currentTag];
204
        } else {
205 6
            $tags = $this->getTags('status');
206
        }
207
208 6
        $options = [];
209 6
        foreach ($tags as $tag) {
0 ignored issues
show
Bug introduced by
The expression $tags of type object<Illuminate\Databa...nteger,{"0":"integer"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
210 6
            $options[ucwords($tag->name)] = [
211 6
                'name'      => 'tag_status',
212 6
                'value'     => $tag->id,
213 6
                'data-tags' => $tag->id,
214 6
                'color'     => $tag->bgcolor,
215
            ];
216
        }
217
218 6
        $fields['tag_status'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$fields was never initialized. Although not strictly required by PHP, it is generally a good practice to add $fields = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
219 6
            'label'  => 'status',
220 6
            'type'   => 'radioButton',
221 6
            'radios' => $options,
222 6
            'check'  => $this->getIssueTag('status')->id,
223
        ];
224
225 6
        return $fields;
226
    }
227
228
    /**
229
     * Returns tags field.
230
     *
231
     * @return array
232
     */
233 7
    protected function fieldTypeTags()
234
    {
235 7
        $currentTag = $this->getIssueTag('type');
236
237 7
        if ($currentTag && !$currentTag->canView()) {
0 ignored issues
show
Bug introduced by
The method canView cannot be called on $currentTag (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
238
            $tags = [$currentTag];
239
        } else {
240 7
            $tags = $this->getTags('type');
241
        }
242
243 7
        $options = [];
244 7
        foreach ($tags as $tag) {
0 ignored issues
show
Bug introduced by
The expression $tags of type object<Illuminate\Databa...nteger,{"0":"integer"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
245 7
            $options[ucwords($tag->name)] = [
246 7
                'name'      => 'tag_type',
247 7
                'value'     => $tag->id,
248 7
                'data-tags' => $tag->id,
249 7
                'color'     => $tag->bgcolor,
250
            ];
251
        }
252
253 7
        $fields['tag_type'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$fields was never initialized. Although not strictly required by PHP, it is generally a good practice to add $fields = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
254 7
            'label'  => 'type',
255 7
            'type'   => 'radioButton',
256 7
            'radios' => $options,
257 7
            'check'  => $this->getIssueTag('type')->id,
258
        ];
259
260 7
        return $fields;
261
    }
262
263
    /**
264
     * Returns tags field.
265
     *
266
     * @return array
267
     */
268 6
    protected function fieldResolutionTags()
269
    {
270 6
        $currentTag = $this->getIssueTag('resolution');
271
272 6
        if ($currentTag && !$currentTag->canView()) {
0 ignored issues
show
Bug introduced by
The method canView cannot be called on $currentTag (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
273
            $tags = [$currentTag];
274
        } else {
275 6
            $tags = $this->getTags('resolution');
276
        }
277
278
        $options = [
279 6
            trans('tinyissue.none') => [
280
                'name'      => 'tag_resolution',
281
                'value'     => 0,
282
                'data-tags' => 0,
283
                'color'     => '#62CFFC',
284 6
            ],
285
        ];
286 6
        foreach ($tags as $tag) {
0 ignored issues
show
Bug introduced by
The expression $tags of type object<Illuminate\Databa...nteger,{"0":"integer"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
287 6
            $options[ucwords($tag->name)] = [
288 6
                'name'      => 'tag_resolution',
289 6
                'value'     => $tag->id,
290 6
                'data-tags' => $tag->id,
291 6
                'color'     => $tag->bgcolor,
292
            ];
293
        }
294
295 6
        $fields['tag_resolution'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$fields was never initialized. Although not strictly required by PHP, it is generally a good practice to add $fields = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
296 6
            'label'  => 'resolution',
297 6
            'type'   => 'radioButton',
298 6
            'radios' => $options,
299 6
            'check'  => $this->getIssueTag('resolution')->id,
300
        ];
301
302 6
        return $fields;
303
    }
304
305
    /**
306
     * Returns assigned to field.
307
     *
308
     * @return array
309
     */
310 6
    protected function fieldAssignedTo()
311
    {
312
        return [
313
            'assigned_to' => [
314 6
                'type'    => 'select',
315 6
                'label'   => 'assigned_to',
316 6
                'options' => [0 => ''] + $this->project->usersCanFixIssue()->get()->lists('fullname', 'id')->all(),
317 6
                'value'   => (int) $this->project->default_assignee,
318
            ],
319
        ];
320
    }
321
322
    /**
323
     * Returns upload field.
324
     *
325
     * @return array
326
     */
327 6
    protected function fieldUpload()
328
    {
329 6
        $user                      = \Auth::guest() ? new Model\User() : \Auth::user();
330 6
        $fields                    = $this->projectUploadFields('upload', $this->project, $user);
331 6
        $fields['upload']['label'] = 'attachments';
332
333 6
        return $fields;
334
    }
335
336
    /**
337
     * Returns time quote field.
338
     *
339
     * @return array
340
     */
341 6
    protected function fieldTimeQuote()
342
    {
343
        $fields = [
344
            'time_quote' => [
345 6
                'type'     => 'groupField',
346 6
                'label'    => 'quote',
347
                'fields'   => [
348
                    'h'    => [
349 6
                        'type'          => 'number',
350 6
                        'append'        => trans('tinyissue.hours'),
351 6
                        'value'         => $this->extractQuoteValue('h'),
352 6
                        'addGroupClass' => 'col-sm-5 col-md-5 col-lg-4',
353
                    ],
354
                    'm'    => [
355 6
                        'type'          => 'number',
356 6
                        'append'        => trans('tinyissue.minutes'),
357 6
                        'value'         => $this->extractQuoteValue('m'),
358 6
                        'addGroupClass' => 'col-sm-5 col-md-5 col-lg-4',
359
                    ],
360
                    'lock' => [
361 6
                        'type'          => 'checkboxButton',
362 6
                        'label'         => '',
363 6
                        'noLabel'       => 1,
364 6
                        'class'         => 'eee',
365 6
                        'value'         => $this->extractQuoteValue('lock'),
366 6
                        'addGroupClass' => 'sss col-sm-12 col-md-12 col-lg-4',
367
                        'checkboxes'    => [
368
                            'Lock Quote' => [
369
                                'value'     => 1,
370
                                'data-tags' => 1,
371
                                'color'     => 'red',
372
                                'checked'   => false,
373
                            ],
374
                        ],
375
                        'grouped'       => true,
376
                    ],
377
                ],
378 6
                'addClass' => 'row issue-quote',
379 6
            ],
380
        ];
381
382
        // If user does not have access to lock quote, then remove the field
383 6
        if (!auth()->user()->permission(Model\Permission::PERM_ISSUE_VIEW_QUOTE)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Auth\Authenticatable as the method permission() does only exist in the following implementations of said interface: Tinyissue\Model\User.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
384 1
            unset($fields['time_quote']['fields']['lock']);
385
386
            // If quote is locked then remove quote fields
387 1
            if ($this->getModel()->isQuoteLocked()) {
388
                return [];
389
            }
390
        }
391
392 6
        return $fields;
393
    }
394
395
    /**
396
     * @return array
397
     */
398 7
    public function rules()
399
    {
400
        $rules = [
401 7
            'title' => 'required|max:200',
402
            'body'  => 'required',
403
        ];
404
405 7
        return $rules;
406
    }
407
408
    /**
409
     * @return string
410
     */
411
    public function getRedirectUrl()
412
    {
413
        if ($this->isEditing()) {
414
            return $this->getModel()->to('edit');
415
        }
416
417
        return 'project/' . $this->project->id . '/issue/new';
418
    }
419
420
    /**
421
     * Extract number of hours, or minutes, or seconds from a quote.
422
     *
423
     * @param string $part
424
     *
425
     * @return float|int
426
     */
427 6
    protected function extractQuoteValue($part)
428
    {
429 6
        if ($this->getModel() instanceof Model\Project\Issue) {
430 2
            $seconds = $this->getModel()->time_quote;
431 2
            if ($part === 'h') {
432 2
                return floor($seconds / 3600);
433
            }
434
435 2
            if ($part === 'm') {
436 2
                return ($seconds / 60) % 60;
437
            }
438
        }
439
440 6
        return 0;
441
    }
442
}
443