Completed
Push — master ( 97f422...f36802 )
by David
02:02
created

CrudApi::__call()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 15
nc 6
nop 2
1
<?php
2
3
namespace Taskforcedev\CrudApi\Helpers;
4
5
use Illuminate\Console\AppNamespaceDetectorTrait;
6
7
class CrudApi
8
{
9
    use AppNamespaceDetectorTrait;
10
11
    public $model;
12
    public $instance;
13
    public $namespace;
14
    public $modelHelper;
15
    public $fieldHelper;
16
17
    public function __construct($options = [])
18
    {
19
        /* Set the namespace */
20
        if (array_key_exists('namespace', $options)) {
21
            $this->namespace = $options['namespace'];
22
        } else {
23
            $this->namespace = $this->getAppNamespace();
24
        }
25
26
        /* Set the model */
27
        if (array_key_exists('model', $options)) {
28
            $this->model = $options['model'];
29
        }
30
31
        $this->modelHelper = new Model($this);
32
        $this->fieldHelper = new Field($this);
33
    }
34
35
    public function setModelHelper($modelHelper)
36
    {
37
        $this->modelHelper = $modelHelper;
38
    }
39
40
    /**
41
     * Set the namespace to search within.
42
     *
43
     * @param string $namespace
44
     */
45
    public function setNamespace($namespace)
46
    {
47
        $this->namespace = $namespace;
48
    }
49
50
    /**
51
     * Set the model to work with.
52
     *
53
     * @param $model
54
     *
55
     * @return $this
56
     */
57
    public function setModel($model)
58
    {
59
        $this->model = $model;
60
61
        return $this;
62
    }
63
64
    /**
65
     * Set a model instance from which to work.
66
     *
67
     * @param $item
68
     *
69
     * @return $this
70
     */
71
    public function setInstance($item)
72
    {
73
        $this->instance = $item;
74
75
        return $this;
76
    }
77
78
    public function getFields()
79
    {
80
        $model = $this->getModelInstance();
81
        $fields = $model->getFillable();
82
        $filtered_fields = [];
83
84
        foreach ($fields as $f) {
85
            if ($f !== 'password' && $f !== 'Password') {
86
                $filtered_fields[] = $f;
87
            }
88
        }
89
90
        return $filtered_fields;
91
    }
92
93
    public function getModelDisplayName()
94
    {
95
        $instance = $this->getModelInstance();
96
        $display = isset($instance->display) ? $instance->display : ucfirst($this->model);
97
98
        return $display;
99
    }
100
101
    public function getModelInstance()
102
    {
103
        if ($this->instance !== null) {
104
            return $this->instance;
105
        }
106
        $model = $this->getModel();
1 ignored issue
show
Bug introduced by
The method getModel() does not exist on Taskforcedev\CrudApi\Helpers\CrudApi. Did you maybe mean getModelDisplayName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
107
        if ($model === false) {
108
            return false;
109
        }
110
        $instance = new $model();
111
112
        return $instance;
113
    }
114
115
    public function modal($type)
116
    {
117
        $trimmed_item = $this->trimmedModel();
118
119
        $modalId = $type.$trimmed_item.'Modal';
120
121
        $display = $this->getModelDisplayName();
122
123
        $modal = (object) [
124
            'id' => $modalId,
125
            'title' => ucfirst($type).' '.$display,
126
            'url' => $this->modalUrl($type),
127
        ];
128
129
        return $modal;
130
    }
131
132
    private function modalUrl($type)
1 ignored issue
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
133
    {
134
        switch ($type) {
135
        case 'edit':
136
            $action = 'update';
137
            break;
138
        default:
139
            $action = $type;
140
            break;
141
        }
142
143
        return route('crudapi.'.$action.'.item', $this->model);
144
    }
145
146
    public function jsMethodName($method)
147
    {
148
        // Method == create
149
        $jsMethod = $method.$this->trimmedModel();
150
151
        return $jsMethod;
152
    }
153
154
    public function renderFields($type, $fields = [])
155
    {
156
        if (empty($fields)) {
157
            $fields = $this->getFields();
158
        }
159
        $output = '';
160
161
        switch ($type) {
162
        case 'form-create':
163
            $output .= $this->fieldHelper->formCreate($fields);
164
            break;
165
        case 'form-edit':
166 View Code Duplication
            foreach ($fields as $f) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
167
                $ucF = ucfirst($f);
168
169
                $input_attr = [
170
                    'class' => 'form-control',
171
                    'id' => 'editItem'.$ucF,
172
                    'name' => $f,
173
                ];
174
175
                $output .= '<fieldset class="form-group">';
176
177
                $output .= '<label for="'.$input_attr['id'].'">'.$ucF.'</label>';
178
179
                if ($this->fieldHelper->isIdField($f)) {
180
                    $input_attr['type'] = 'select';
181
182
                    $output .= '<select ';
183
                    foreach ($input_attr as $attr => $value) {
184
                        $output .= "{$attr}='{$value}'";
185
                    }
186
187
                    $relation = $this->getRelatedModel($f);
188
                    $output .= '>';
189
190
                    $output .= $this->getRelatedOptions($relation);
191
                    $output .= '</select>';
192
                } else {
193
                    $input_attr['type'] = 'text';
194
195
                    $output .= '<input ';
196
                    foreach ($input_attr as $attr => $value) {
197
                        $output .= "{$attr}='{$value}'";
198
                    }
199
                    $output .= '>';
200
                }
201
202
                $output .= '</fieldset>';
203
            }
204
            break;
205
        case 'table-headings':
206
            $output .= $this->fieldHelper->tableHeadings($fields);
207
            break;
208
        case 'table-content':
209
            $output .= $this->fieldHelper->tableContent($fields, $this->instance);
210
            break;
211
            // JavaScript Variables
212
        case 'js-var':
213
            foreach ($fields as $f) {
214
                $output .= 'var '.$f.' = '.$this->instance->$f.'; ';
215
            }
216
            break;
217
        case 'js-modal-create':
218
            foreach ($fields as $f) {
219
                $output .= '"'.$f.'": $(\'#createItem'.$f.'\').val(), ';
220
            }
221
            break;
222
        default:
223
            return;
224
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
225
        }
226
227
        echo $output;
228
    }
229
230
    public function trimmedModel()
231
    {
232
        return strpos($this->model, ' ') !== false ? implode('', explode(' ', $this->model)) : $this->model;
233
    }
234
235
    public function getRelatedOptions($relation)
236
    {
237
        $output = '';
238
239
        if (!method_exists($relation, 'toOptions')) {
240
            $relationItems = $relation::all();
241
            foreach ($relationItems as $item) {
242
                if (!isset($item->name)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
243
                    // Error - there is no toOptions or name property.
244
                } else {
245
                    $output .= '<option value="'.$item->id.'">'.$item->name.'</option>';
246
                }
247
            }
248
        } else {
249
            $output .= $relation->toOptions();
250
        }
251
252
        return $output;
253
    }
254
255
    public function getRelatedModel($f)
256
    {
257
        $field = $this->getRelatedField($f);
0 ignored issues
show
Documentation Bug introduced by
The method getRelatedField does not exist on object<Taskforcedev\CrudApi\Helpers\CrudApi>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
258
        $model = $this->getModel($field);
0 ignored issues
show
Bug introduced by
The method getModel() does not exist on Taskforcedev\CrudApi\Helpers\CrudApi. Did you maybe mean getModelDisplayName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
259
260
        $modelAliases = [
261
            'author' => 'user',
262
        ];
263
264
        // If class doesn't exist, check if is in aliases array.
265
        if (!class_exists($model)) {
266
            if (array_key_exists($field, $modelAliases)) {
267
                $aliasedModel = $modelAliases[$field];
268
                $model = $this->getModel($aliasedModel);
0 ignored issues
show
Bug introduced by
The method getModel() does not exist on Taskforcedev\CrudApi\Helpers\CrudApi. Did you maybe mean getModelDisplayName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
269
            }
270
        }
271
272
        // Model could not be found, try via it's converted name.
273
        if (!class_exists($model)) {
274
            // Convert from DB format to Pascal
275
            $words = explode('_', $field);
276
            foreach ($words as $i => $w) {
277
                $words[$i] = ucfirst($w);
278
            }
279
            $formatted = implode('', $words);
280
            $model = 'App\\'.$formatted;
281
            if (!class_exists($model)) {
282
                return false;
283
            }
284
        }
285
286
        return new $model();
287
    }
288
289
    public function getRelatedDisplay($f)
290
    {
291
        $related_field = $this->getRelatedField($f);
0 ignored issues
show
Documentation Bug introduced by
The method getRelatedField does not exist on object<Taskforcedev\CrudApi\Helpers\CrudApi>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
292
293
        $field = $this->instance->$related_field;
294
295
        $class = get_class($field);
296
297
        switch ($class) {
298
        case 'App\\Helpers\\CrudApi':
299
            break;
300
        case 'App\\Indicator':
301
            return $field->indicator;
302
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
303
        case 'Taskforcedev\\CrudApi\\Helpers\\CrudApi':
304
            return false;
305
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
306
        default:
307
            return $field->name;
308
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
309
        }
310
    }
311
312
    /**
313
     * Allow certain methods to be passed through to the specified
314
     * helper in order to make methods easier to remember.
315
     *
316
     * @param $method
317
     * @param $args
318
     *
319
     * @return bool
320
     */
321
    public function __call($method, $args)
322
    {
323
        switch ($method) {
324
            case 'getRelatedField':
325
                return $this->fieldHelper->getRelatedField($args[0]);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->fieldHelpe...RelatedField($args[0]); (string) is incompatible with the return type documented by Taskforcedev\CrudApi\Helpers\CrudApi::__call of type boolean.

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...
326
            case 'getPrimaryField':
327
                return $this->fieldHelper->getPrimaryField($args[0]);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->fieldHelpe...PrimaryField($args[0]); (string) is incompatible with the return type documented by Taskforcedev\CrudApi\Helpers\CrudApi::__call of type boolean.

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...
328
            case 'isIdField':
329
                return $this->fieldHelper->isIdField($args[0]);
330
            case 'getModel':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
331
                if (isset($args[0])) {
332
                    return $this->modelHelper->getModel($args[0]);
333
                } else {
334
                    return $this->modelHelper->getModel();
335
                }
336
            default:
337
                break;
338
        }
339
    }
340
}
341