Test Failed
Push — ft/states ( 7e5f34...a5a3f4 )
by Ben
06:21
created

AbstractManager   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 96.12%

Importance

Changes 7
Bugs 3 Features 0
Metric Value
wmc 43
eloc 97
c 7
b 3
f 0
dl 0
loc 288
ccs 99
cts 103
cp 0.9612
rs 8.96

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A fieldsWithAssistantFields() 0 13 3
A managerKey() 0 3 1
A hasExistingModel() 0 3 2
A guard() 0 7 2
A route() 0 21 2
A storeRequest() 0 3 1
A validateConstraints() 0 4 2
A indexSorting() 0 12 4
A can() 0 10 3
A delete() 0 13 3
A updateRequest() 0 3 1
A indexPagination() 0 9 1
A filters() 0 3 1
A findManaged() 0 5 1
A bootTraitMethods() 0 16 5
A manage() 0 5 1
A indexCollection() 0 18 2
A fieldArrangement() 0 3 1
A fields() 0 3 1
A existingModel() 0 7 2
A modelClass() 0 3 1
A requestContainsTranslations() 0 3 1
A indexBuilder() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Thinktomorrow\Chief\Management;
4
5
use Illuminate\Contracts\Pagination\Paginator;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Http\Request;
8
use Thinktomorrow\Chief\Concerns\Translatable\TranslatableCommand;
9
use Thinktomorrow\Chief\Fields\FieldArrangement;
10
use Thinktomorrow\Chief\Fields\Fields;
11
use Thinktomorrow\Chief\Fields\RenderingFields;
12
use Thinktomorrow\Chief\Fields\SavingFields;
13
use Thinktomorrow\Chief\Filters\Filters;
14
use Thinktomorrow\Chief\Management\Assistants\ManagesAssistants;
15
use Thinktomorrow\Chief\Management\Details\HasDetails;
16
use Thinktomorrow\Chief\Management\Details\HasSections;
17
use Thinktomorrow\Chief\Management\Exceptions\NonExistingRecord;
18
use Thinktomorrow\Chief\Management\Exceptions\NotAllowedManagerRoute;
19
use Thinktomorrow\Chief\Relations\ActsAsChild;
20
use Thinktomorrow\Chief\Relations\ActsAsParent;
21
22
abstract class AbstractManager
23
{
24
    use RenderingFields,
0 ignored issues
show
Bug introduced by
The trait Thinktomorrow\Chief\Fields\SavingFields requires the property $key which is not provided by Thinktomorrow\Chief\Management\AbstractManager.
Loading history...
introduced by
The trait Thinktomorrow\Chief\Management\Details\HasDetails requires some properties which are not provided by Thinktomorrow\Chief\Management\AbstractManager: $labelPlural, $id, $labelSingular, $title
Loading history...
introduced by
The trait Thinktomorrow\Chief\Management\ManagesPagebuilder requires some properties which are not provided by Thinktomorrow\Chief\Management\AbstractManager: $trans, $slug, $page_id, $id
Loading history...
25
        SavingFields,
26
        HasDetails,
27
        HasSections,
28
        ManagesMedia,
29
        ManagesPagebuilder,
30
        TranslatableCommand,
31
        ManagesAssistants;
32
33
    protected $translation_columns = [];
34
35
    protected $model;
36
37
    /** @var Register */
38
    protected $registration;
39
40
    protected $pageCount = 20;
41
    protected $paginated = true;
42
    protected static $bootedTraitMethods = [];
43
44 175
    public function __construct(Registration $registration)
45
    {
46 175
        $this->registration = $registration;
0 ignored issues
show
Documentation Bug introduced by
It seems like $registration of type Thinktomorrow\Chief\Management\Registration is incompatible with the declared type Thinktomorrow\Chief\Management\Register of property $registration.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
47
48
        // Upon instantiation, a general model is set that doesn't point to a persisted record.
49 175
        $this->manage(app($this->registration->model()));
50
51
        // Check if key and model are present since the model should be set by the manager itself
52 175
        $this->validateConstraints();
53
54 175
        static::bootTraitMethods();
55 175
    }
56
57 175
    public function managerKey(): string
58
    {
59 175
        return $this->registration->key();
0 ignored issues
show
Bug introduced by
The method key() does not exist on Thinktomorrow\Chief\Management\Register. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

59
        return $this->registration->/** @scrutinizer ignore-call */ key();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
60
    }
61 175
62
    public function manage($model): Manager
63
    {
64 104
        $this->model = $model;
65
66 104
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Thinktomorrow\Chief\Management\AbstractManager which is incompatible with the type-hinted return Thinktomorrow\Chief\Management\Manager.
Loading history...
67
    }
68 104
69
    public function findManaged($id): Manager
70 104
    {
71
        $modelInstance = $this->modelClass()::where('id', $id)->withoutGlobalScopes()->first();
72
73 6
        return (new static($this->registration))->manage($modelInstance);
0 ignored issues
show
Bug introduced by
$this->registration of type Thinktomorrow\Chief\Management\Register is incompatible with the type Thinktomorrow\Chief\Management\Registration expected by parameter $registration of Thinktomorrow\Chief\Mana...tManager::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

73
        return (new static(/** @scrutinizer ignore-type */ $this->registration))->manage($modelInstance);
Loading history...
74
    }
75 6
76
    public function indexCollection()
77 6
    {
78
        $modelClass = $this->modelClass();
79 6
80
        $builder = (new $modelClass)->query();
81 6
82
        $this->filters()->apply($builder);
83 6
84
        $builder = $this->indexBuilder($builder);
85 6
86 6
        $builder = $this->indexSorting($builder);
87
88
        if ($this->paginated) {
89
            return $this->indexPagination($builder);
90
        }
91
92
        return $builder->get()->map(function ($model) {
93
            return (new static($this->registration))->manage($model);
0 ignored issues
show
Bug introduced by
$this->registration of type Thinktomorrow\Chief\Management\Register is incompatible with the type Thinktomorrow\Chief\Management\Registration expected by parameter $registration of Thinktomorrow\Chief\Mana...tManager::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
            return (new static(/** @scrutinizer ignore-type */ $this->registration))->manage($model);
Loading history...
94 6
        });
95
    }
96 6
97
    protected function indexBuilder(Builder $builder): Builder
98
    {
99 6
        return $builder;
100
    }
101 6
102 3
    protected function indexSorting(Builder $builder): Builder
103
    {
104
        if ($this->isAssistedBy('publish')) {
105
            $builder->orderBy('published_at', 'DESC');
106 6
        }
107 6
108
        // if model has no timestamps, updated_at doesn't exist
109
        if (($class = $this->modelClass()) && (new $class)->timestamps) {
110 6
            $builder->orderBy('updated_at', 'DESC');
111
        }
112
113 6
        return $builder;
114
    }
115 6
116
    protected function indexPagination($builder): Paginator
117
    {
118 4
        $paginator = $builder->paginate($this->pageCount);
119 6
120
        $modifiedCollection = $builder->paginate($this->pageCount)->getCollection()->transform(function ($model) {
121 6
            return (new static($this->registration))->manage($model);
0 ignored issues
show
Bug introduced by
$this->registration of type Thinktomorrow\Chief\Management\Register is incompatible with the type Thinktomorrow\Chief\Management\Registration expected by parameter $registration of Thinktomorrow\Chief\Mana...tManager::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
            return (new static(/** @scrutinizer ignore-type */ $this->registration))->manage($model);
Loading history...
122
        });
123
124 104
        return $paginator->setCollection($modifiedCollection);
125
    }
126 104
127
    public function modelClass(): string
128
    {
129 120
        return $this->registration->model();
0 ignored issues
show
Bug introduced by
The method model() does not exist on Thinktomorrow\Chief\Management\Register. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

129
        return $this->registration->/** @scrutinizer ignore-call */ model();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
130
    }
131 120
132
    public function existingModel(): ManagedModel
133
    {
134
        if (!$this->hasExistingModel()) {
135
            throw new NonExistingRecord('Model does not exist yet but is expected.');
136
        }
137
138
        return $this->model;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->model could return the type Illuminate\Contracts\Foundation\Application which is incompatible with the type-hinted return Thinktomorrow\Chief\Management\ManagedModel. Consider adding an additional type-check to rule them out.
Loading history...
139
    }
140 120
141
    public function hasExistingModel(): bool
142 120
    {
143 1
        return ($this->model && $this->model->exists);
144
    }
145
146 119
    /**
147
     * Determine which actions should be available for this
148
     * manager and their respective routed urls.
149
     *
150
     * @param $verb
151
     * @return null|string
152
     * @throws NonExistingRecord
153
     */
154
    public function route($verb): ?string
155
    {
156
        $routes = [
157 130
            'index'   => route('chief.back.managers.index', [$this->registration->key()]),
158
            'create'  => route('chief.back.managers.create', [$this->registration->key()]),
159
            'store'   => route('chief.back.managers.store', [$this->registration->key()]),
160 130
        ];
161 130
162 130
        if (array_key_exists($verb, $routes)) {
163
            return $routes[$verb] ?? null;
164
        }
165 130
166 75
        //These routes expect the model to be persisted in the database
167
        $modelRoutes = [
168
            'edit'    => route('chief.back.managers.edit', [$this->registration->key(), $this->existingModel()->id]),
0 ignored issues
show
Bug introduced by
Accessing id on the interface Thinktomorrow\Chief\Management\ManagedModel suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
169
            'update'  => route('chief.back.managers.update', [$this->registration->key(), $this->existingModel()->id]),
170
            'delete'  => route('chief.back.managers.delete', [$this->registration->key(), $this->existingModel()->id]),
171 108
            'upload'  => route('chief.back.managers.media.upload', [$this->registration->key(), $this->existingModel()->id]),
172 107
        ];
173 107
174 107
        return $modelRoutes[$verb] ?? null;
175
    }
176
177 107
    public function can($verb): bool
178
    {
179
        foreach (static::$bootedTraitMethods['can'] as $method) {
180 141
            if (!method_exists($this, $method)) {
181
                continue;
182 141
            }
183 8
            $this->$method($verb);
184
        }
185
186 8
        return !is_null($this->route($verb));
187
    }
188
189 137
    public function guard($verb): Manager
190
    {
191
        if (! $this->can($verb)) {
192 127
            NotAllowedManagerRoute::notAllowedVerb($verb, $this);
193
        }
194 127
195 5
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Thinktomorrow\Chief\Management\AbstractManager which is incompatible with the type-hinted return Thinktomorrow\Chief\Management\Manager.
Loading history...
196
    }
197
198 122
    public function fields(): Fields
199
    {
200
        return new Fields();
201 87
    }
202
203 87
    /**
204
     * Enrich the manager fields with any of the assistant specified fields
205
     *
206
     * @return Fields
207
     * @throws \Exception
208
     */
209
    public function fieldsWithAssistantFields(): Fields
210
    {
211
        $fields = $this->fields();
212 110
213
        foreach ($this->assistants() as $assistant) {
214 110
            if (! method_exists($assistant, 'fields')) {
215
                continue;
216 110
            }
217 84
218 82
            $fields = $fields->merge($assistant->fields());
219
        }
220
221 84
        return $fields;
222
    }
223
224 110
    /**
225
     * This determines the arrangement of the manageable fields
226
     * on the create and edit forms. By default, all fields
227
     * are presented in their order of appearance
228
     *
229
     * @param null $key pinpoint to a specific field arrangement e.g. for create page.
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $key is correct as it would always require null to be passed?
Loading history...
230
     * @return FieldArrangement
231
     * @throws \Exception
232
     */
233
    public function fieldArrangement($key = null): FieldArrangement
234
    {
235
        return new FieldArrangement($this->fieldsWithAssistantFields());
236 3
    }
237
238 3
    public function delete()
239
    {
240
        $this->guard('delete');
241 3
242
        if ($this->model instanceof ActsAsChild) {
243 3
            $this->model->detachAllParentRelations();
244
        }
245 2
246 2
        if ($this->model instanceof ActsAsParent) {
247
            $this->model->detachAllChildRelations();
248
        }
249 2
250 2
        $this->model->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Illuminate\Contracts\Foundation\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

250
        $this->model->/** @scrutinizer ignore-call */ 
251
                      delete();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
251
    }
252
253 2
    public static function filters(): Filters
254 2
    {
255
        return new Filters();
256 3
    }
257
258 3
    /**
259
     * This method can be used to manipulate the store request payload
260
     * before being passed to the storing / updating the models.
261
     *
262
     * @param Request $request
263
     * @return Request
264
     */
265
    public function storeRequest(Request $request): Request
266
    {
267
        return $request;
268 26
    }
269
270 26
    /**
271
     * This method can be used to manipulate the update request payload
272
     * before being passed to the storing / updating the models.
273
     *
274
     * @param Request $request
275
     * @return Request
276
     */
277
    public function updateRequest(Request $request): Request
278
    {
279
        return $request;
280 24
    }
281
282 24
    protected function requestContainsTranslations(Request $request): bool
283
    {
284
        return $request->has('trans');
285 83
    }
286
287 83
    protected function validateConstraints()
288
    {
289
        if (!$this->model) {
290 175
            throw new \DomainException('Model class should be set for this manager. Please set the model property default via the constructor or by extending the setupDefaults method.');
291
        }
292 175
    }
293
294
    public static function bootTraitMethods()
295 175
    {
296
        $class = static::class;
297 175
298
        $methods = [
299 175
            'can'
300
        ];
301
302 175
        foreach ($methods as $baseMethod) {
303
            static::$bootedTraitMethods[$baseMethod] = [];
304
305 175
            foreach (class_uses_recursive($class) as $trait) {
306 175
                $method = class_basename($trait) . ucfirst($baseMethod);
307
308 175
                if (method_exists($class, $method) && ! in_array($method, static::$bootedTraitMethods[$baseMethod])) {
309 175
                    static::$bootedTraitMethods[$baseMethod][] = lcfirst($method);
310
                }
311 175
            }
312 175
        }
313
    }
314
}
315