Completed
Push — develop ( 335c93...e4583a )
by Mohamed
12:58
created

Issue::fields()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0052

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 22
ccs 11
cts 12
cp 0.9167
rs 9.2
cc 3
eloc 12
nc 4
nop 0
crap 3.0052
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
     * Is issue readonly.
39
     *
40
     * @var bool
41
     */
42
    protected $readOnly = false;
43
44
    /**
45
     * @param string $type
46
     *
47
     * @return \Illuminate\Database\Eloquent\Collection|null
48
     */
49 2 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...
50
    {
51 2
        if ($this->tags === null) {
52 2
            $this->tags = (new Model\Tag())->getGroupTags();
53
        }
54
55 2
        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...
56 2
            return $this->tags->where('name', $type)->first()->tags;
57
        }
58
59
        return $this->tags;
60
    }
61
62
    /**
63
     * Returns an array of tags to be used as the selectable options.
64
     *
65
     * @param string $type
66
     *
67
     * @return array
68
     */
69 2
    protected function getSelectableTags($type = null)
70
    {
71 2
        $currentTag = $this->getIssueTag($type);
72
73 2
        if ($currentTag->id && (!$currentTag->canView() || $this->readOnly)) {
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...
74 1
            $tags = [$currentTag];
75 2
        } elseif ($this->readOnly) {
76 1
            $tags = [];
77
        } else {
78 2
            $tags = $this->getTags($type);
79
        }
80
81 2
        $options = [];
82 2
        foreach ($tags as $tag) {
0 ignored issues
show
Bug introduced by
The expression $tags of type object<Illuminate\Databa...\Collection>|null|array 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...
83 2
            $options[ucwords($tag->name)] = [
84 2
                'name'      => 'tag_' . $type,
85 2
                'value'     => $tag->id,
86 2
                'data-tags' => $tag->id,
87 2
                'color'     => $tag->bgcolor,
88
            ];
89
        }
90
91 2
        return $options;
92
    }
93
94
    /**
95
     * Get issue tag for specific type/group.
96
     *
97
     * @param string $type
98
     *
99
     * @return int
100
     */
101 2
    protected function getIssueTag($type)
102
    {
103 2
        if ($this->isEditing()) {
104 2
            $groupId     = $this->getTags($type)->first()->parent_id;
105 2
            $selectedTag = $this->getModel()->tags->where('parent_id', $groupId);
106
107 2
            if ($selectedTag->count() > 0) {
108 1
                return $selectedTag->last();
109
            }
110
        }
111
112 2
        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...
113
    }
114
115
    /**
116
     * @param array $params
117
     *
118
     * @return void
119
     */
120 6
    public function setup(array $params)
121
    {
122 6
        $this->project = $params['project'];
123 6
        if (!empty($params['issue'])) {
124 2
            $this->editingModel($params['issue']);
125 2
            $this->readOnly = $this->getModel()->hasReadOnlyTag($this->user);
126
        }
127 6
    }
128
129
    /**
130
     * @return array
131
     */
132
    public function actions()
133
    {
134
        $actions = [];
135
136
        // Check if issue is in readonly tag
137
        if (!$this->readOnly) {
138
            $actions = [
139
                'submit' => $this->isEditing() ? 'update_issue' : 'create_issue',
140
            ];
141
142
            if ($this->isEditing() && $this->user->permission(Model\Permission::PERM_ISSUE_MODIFY)) {
143
                $actions['delete'] = [
144
                    'type'         => 'danger_submit',
145
                    'label'        => trans('tinyissue.delete_something', ['name' => '#' . $this->getModel()->id]),
146
                    'class'        => 'close-issue',
147
                    'name'         => 'delete-issue',
148
                    'data-message' => trans('tinyissue.delete_issue_confirm'),
149
                ];
150
            }
151
        }
152
153
        return $actions;
154
    }
155
156
    /**
157
     * @return array
158
     */
159 6
    public function fields()
160
    {
161 6
        $issueModify = $this->user->permission('issue-modify');
162
163 2
        $fields = [];
164 2
        $fields += $this->readOnlyMessage();
165 2
        $fields += $this->fieldTitle();
166 2
        $fields += $this->fieldBody();
167 2
        $fields += $this->fieldTypeTags();
168
169
        // Only on creating new issue
170 2
        if (!$this->isEditing()) {
171
            $fields += $this->fieldUpload();
172
        }
173
174
        // Show fields for users with issue modify permission
175 2
        if ($issueModify) {
176 2
            $fields += $this->issueModifyFields();
177
        }
178
179 2
        return $fields;
180
    }
181
182
    /**
183
     * Return a list of fields for users with issue modify permission.
184
     *
185
     * @return array
186
     */
187 2
    protected function issueModifyFields()
188
    {
189 2
        $fields = [];
190
191 2
        $fields['internal_status'] = [
192
            'type' => 'legend',
193
        ];
194
195
        // Status tags
196 2
        $fields += $this->fieldStatusTags();
197
198
        // Assign users
199 2
        $fields += $this->fieldAssignedTo();
200
201
        // Quotes
202 2
        $fields += $this->fieldTimeQuote();
203
204
        // Resolution tags
205 2
        $fields += $this->fieldResolutionTags();
206
207 2
        return $fields;
208
    }
209
210
    /**
211
     * Returns message about read only issue.
212
     *
213
     * @return array
214
     */
215 2
    protected function readOnlyMessage()
216
    {
217 2
        $field = [];
218
219 2
        if ($this->readOnly) {
220
            $field = [
221
                'readonly' => [
222 1
                    'type'  => 'plaintext',
223 1
                    'label' => ' ',
224 1
                    'value' => '<div class="alert alert-warning">' . trans('tinyissue.readonly_issue_message') . '</div>',
225
                ],
226
            ];
227
        }
228
229 2
        return $field;
230
    }
231
232
    /**
233
     * Returns title field.
234
     *
235
     * @return array
236
     */
237 2
    protected function fieldTitle()
238
    {
239
        return [
240
            'title' => [
241
                'type'  => 'text',
242
                'label' => 'title',
243 2
            ],
244
        ];
245
    }
246
247
    /**
248
     * Returns body field.
249
     *
250
     * @return array
251
     */
252 2
    protected function fieldBody()
253
    {
254
        return [
255
            'body' => [
256
                'type'  => 'textarea',
257
                'label' => 'issue',
258 2
            ],
259
        ];
260
    }
261
262
    /**
263
     * Returns status tag field.
264
     *
265
     * @return array
266
     */
267 2 View Code Duplication
    protected function fieldStatusTags()
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...
268
    {
269 2
        $options = $this->getSelectableTags('status');
270
271 2
        $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...
272 2
            'label'  => 'status',
273 2
            'type'   => 'radioButton',
274 2
            'radios' => $options,
275 2
            'check'  => $this->getIssueTag('status')->id,
276
        ];
277
278 2
        return $fields;
279
    }
280
281
    /**
282
     * Returns tags field.
283
     *
284
     * @return array
285
     */
286 2 View Code Duplication
    protected function fieldTypeTags()
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...
287
    {
288 2
        $options = $this->getSelectableTags('type');
289
290 2
        $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...
291 2
            'label'  => 'type',
292 2
            'type'   => 'radioButton',
293 2
            'radios' => $options,
294 2
            'check'  => $this->getIssueTag('type')->id,
295
        ];
296
297 2
        return $fields;
298
    }
299
300
    /**
301
     * Returns tags field.
302
     *
303
     * @return array
304
     */
305 2
    protected function fieldResolutionTags()
306
    {
307 2
        $options = $this->getSelectableTags('resolution');
308
309 2
        if (!empty($options)) {
310
            $options = [
311 2
                    trans('tinyissue.none') => [
312
                        'name'      => 'tag_resolution',
313
                        'value'     => 0,
314
                        'data-tags' => 0,
315
                        'color'     => '#62CFFC',
316 2
                    ],
317 2
                ] + $options;
318
        }
319
320 2
        $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...
321 2
            'label'  => 'resolution',
322 2
            'type'   => 'radioButton',
323 2
            'radios' => $options,
324 2
            'check'  => $this->getIssueTag('resolution')->id,
325
        ];
326
327 2
        return $fields;
328
    }
329
330
    /**
331
     * Returns assigned to field.
332
     *
333
     * @return array
334
     */
335 2
    protected function fieldAssignedTo()
336
    {
337
        return [
338
            'assigned_to' => [
339 2
                'type'    => 'select',
340 2
                'label'   => 'assigned_to',
341 2
                'options' => [0 => ''] + $this->project->usersCanFixIssue()->get()->lists('fullname', 'id')->all(),
342 2
                'value'   => (int) $this->project->default_assignee,
343
            ],
344
        ];
345
    }
346
347
    /**
348
     * Returns upload field.
349
     *
350
     * @return array
351
     */
352
    protected function fieldUpload()
353
    {
354
        $user                      = !$this->user ? new Model\User() : $this->user;
355
        $fields                    = $this->projectUploadFields('upload', $this->project, $user);
356
        $fields['upload']['label'] = 'attachments';
357
358
        return $fields;
359
    }
360
361
    /**
362
     * Returns time quote field.
363
     *
364
     * @return array
365
     */
366 2
    protected function fieldTimeQuote()
367
    {
368
        $fields = [
369
            'time_quote' => [
370 2
                'type'     => 'groupField',
371 2
                'label'    => 'quote',
372
                'fields'   => [
373
                    'h'    => [
374 2
                        'type'          => 'number',
375 2
                        'append'        => trans('tinyissue.hours'),
376 2
                        'value'         => $this->extractQuoteValue('h'),
377 2
                        'addGroupClass' => 'col-sm-5 col-md-5 col-lg-4',
378
                    ],
379
                    'm'    => [
380 2
                        'type'          => 'number',
381 2
                        'append'        => trans('tinyissue.minutes'),
382 2
                        'value'         => $this->extractQuoteValue('m'),
383 2
                        'addGroupClass' => 'col-sm-5 col-md-5 col-lg-4',
384
                    ],
385
                    'lock' => [
386 2
                        'type'          => 'checkboxButton',
387 2
                        'label'         => '',
388
                        'noLabel'       => true,
389 2
                        'class'         => 'eee',
390 2
                        'addGroupClass' => 'sss col-sm-12 col-md-12 col-lg-4',
391
                        'checkboxes'    => [
392
                            'Lock Quote' => [
393 2
                                'value'     => 1,
394 2
                                'data-tags' => 1,
395 2
                                'color'     => 'red',
396 2
                                'checked'   => $this->isEditing() && $this->getModel()->isQuoteLocked(),
397
                            ],
398
                        ],
399
                        'grouped'       => true,
400
                    ],
401
                ],
402 2
                'addClass' => 'row issue-quote',
403 2
            ],
404
        ];
405
406
        // If user does not have access to lock quote, then remove the field
407 2
        if (!$this->user->permission(Model\Permission::PERM_ISSUE_LOCK_QUOTE)) {
408 2
            unset($fields['time_quote']['fields']['lock']);
409
410
            // If quote is locked then remove quote fields
411 2
            if ($this->isEditing() && $this->getModel()->isQuoteLocked()) {
412 1
                return [];
413
            }
414
        }
415
416 2
        return $fields;
417
    }
418
419
    /**
420
     * @return array
421
     */
422 5
    public function rules()
423
    {
424
        $rules = [
425 5
            'title' => 'required|max:200',
426
            'body'  => 'required',
427
        ];
428
429 5
        return $rules;
430
    }
431
432
    /**
433
     * @return string
434
     */
435
    public function getRedirectUrl()
436
    {
437
        if ($this->isEditing()) {
438
            return $this->getModel()->to('edit');
439
        }
440
441
        return 'project/' . $this->project->id . '/issue/new';
442
    }
443
444
    /**
445
     * Extract number of hours, or minutes, or seconds from a quote.
446
     *
447
     * @param string $part
448
     *
449
     * @return float|int
450
     */
451 2
    protected function extractQuoteValue($part)
452
    {
453 2
        if ($this->getModel() instanceof Model\Project\Issue) {
454 2
            $seconds = $this->getModel()->time_quote;
455 2
            if ($part === 'h') {
456 2
                return floor($seconds / 3600);
457
            }
458
459 2
            if ($part === 'm') {
460 2
                return ($seconds / 60) % 60;
461
            }
462
        }
463
464
        return 0;
465
    }
466
}
467