Completed
Push — master ( e5ab12...9197f0 )
by Konstantinos
09:09
created

CRUDController   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 320
Duplicated Lines 6.56 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 30.69%
Metric Value
wmc 28
lcom 1
cbo 12
dl 21
loc 320
ccs 31
cts 101
cp 0.3069
rs 10

13 Methods

Rating   Name   Duplication   Size   Complexity  
A validate() 0 3 1
B edit() 10 22 4
A canCreate() 0 6 1
A canEdit() 0 4 1
A getMessages() 0 59 1
A redirectTo() 0 8 2
A validateNew() 0 3 1
B delete() 0 34 5
B create() 11 23 4
A canDelete() 0 4 1
A redirectToList() 0 7 1
A getFormCreator() 0 10 2
B getMessage() 0 26 4

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
use BZIon\Form\Creator\ModelFormCreator;
4
use Symfony\Component\Form\Form;
5
use Symfony\Component\HttpFoundation\RedirectResponse;
6
7
/**
8
 * A controller with actions for creating, reading, updating and deleting models
9
 * @package BZiON\Controllers
10
 */
11
abstract class CRUDController extends JSONController
12
{
13
    /**
14
     * Make sure that the data of a form is valid, only called when creating a
15
     * new object
16
     * @param  Form $form The submitted form
17
     * @return void
18
     */
19
    protected function validateNew($form)
20
    {
21
    }
22
23
    /**
24
     * Make sure that the data of a form is valid
25
     * @param  Form $form The submitted form
26
     * @return void
27
     */
28
    protected function validate($form)
29
    {
30
    }
31
32
    /**
33
     * Delete a model
34
     * @param  PermissionModel    $model     The model we want to delete
35
     * @param  Player             $me        The user who wants to delete the model
36
     * @param  Closure|null       $onSuccess Something to do when the model is deleted
37
     * @throws ForbiddenException
38
     * @return mixed              The response to show to the user
39
     */
40
    protected function delete(PermissionModel $model, Player $me, $onSuccess = null)
41
    {
42
        if ($model->isDeleted()) {
43
            // We will have to hard delete the model
44
            $hard = true;
45
            $message = 'hardDelete';
46
            $action = 'Erase forever';
47
        } else {
48
            $hard = false;
49
            $message = 'softDelete';
50
            $action = 'Delete';
51
        }
52
53
        if (!$this->canDelete($me, $model, $hard)) {
54
            throw new ForbiddenException($this->getMessage($model, $message, 'forbidden'));
55
        }
56
57
        $successMessage = $this->getMessage($model, $message, 'success');
58
        $redirection    = $this->redirectToList($model);
59
60
        return $this->showConfirmationForm(function () use ($model, $hard, $redirection, $onSuccess) {
61
            if ($hard) {
62
                $model->wipe();
63
            } else {
64
                $model->delete();
65
            }
66
67
            if ($onSuccess) {
68
                $onSuccess();
69
            }
70
71
            return $redirection;
72
        }, $this->getMessage($model, $message, 'confirm'), $successMessage, $action);
73
    }
74
75
    /**
76
     * Create a model
77
     *
78
     * This method requires that you have implemented enter() and a form creator
79
     * for the model
80
     *
81
     * @param  Player             $me The user who wants to create the model
82
     * @throws ForbiddenException
83
     * @return mixed              The response to show to the user
84
     */
85 1
    protected function create(Player $me)
86
    {
87 1
        if (!$this->canCreate($me)) {
88 1
            throw new ForbiddenException($this->getMessage($this->getName(), 'create', 'forbidden'));
89
        }
90
91
        $creator = $this->getFormCreator();
92
        $form = $creator->create()->handleRequest($this->getRequest());
93
94 View Code Duplication
        if ($form->isSubmitted()) {
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...
95
            $this->validate($form);
96
            $this->validateNew($form);
97
            if ($form->isValid()) {
98
                $model = $creator->enter($form);
99
                $this->getFlashBag()->add("success",
100
                     $this->getMessage($model, 'create', 'success'));
101
102
                return $this->redirectTo($model);
103
            }
104
        }
105
106
        return array("form" => $form->createView());
107
    }
108
109
    /**
110
     * Edit a model
111
     *
112
     * This method requires that you have implemented update() and a form creator
113
     * for the model
114
     *
115
     * @param  PermissionModel    $model The model we want to edit
116
     * @param  Player             $me    The user who wants to edit the model
117
     * @param  string             $type  The name of the variable to pass to the view
118
     * @throws ForbiddenException
119
     * @return mixed              The response to show to the user
120
     */
121
    protected function edit(PermissionModel $model, Player $me, $type)
122
    {
123
        if (!$this->canEdit($me, $model)) {
124
            throw new ForbiddenException($this->getMessage($model, 'edit', 'forbidden'));
125
        }
126
127
        $creator = $this->getFormCreator($model);
128
        $form = $creator->create()->handleRequest($this->getRequest());
129
130 View Code Duplication
        if ($form->isSubmitted()) {
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...
131
            $this->validate($form);
132
            if ($form->isValid()) {
133
                $creator->update($form, $model);
134
                $this->getFlashBag()->add("success",
135
                    $this->getMessage($model, 'edit', 'success'));
136
137
                return $this->redirectTo($model);
138
            }
139
        }
140
141
        return array("form" => $form->createView(), $type => $model);
142
    }
143
144
    /**
145
     * Find whether a player can delete a model
146
     *
147
     * @param  Player          $player The player who wants to delete the model
148
     * @param  PermissionModel $model  The model that will be deleted
149
     * @param  bool         $hard   Whether to hard-delete the model instead of soft-deleting it
150
     * @return bool
151
     */
152
    protected function canDelete($player, $model, $hard = false)
153
    {
154
        return $player->canDelete($model, $hard);
155
    }
156
157
    /**
158
     * Find whether a player can create a model
159
     *
160
     * @param  Player  $player The player who wants to create a model
161
     * @return bool
162
     */
163 1
    protected function canCreate($player)
164
    {
165 1
        $modelName = $this->getName();
166
167 1
        return $player->canCreate($modelName);
168
    }
169
170
    /**
171
     * Find whether a player can edit a model
172
     *
173
     * @param  Player          $player The player who wants to delete the model
174
     * @param  PermissionModel $model  The model which will be edited
175
     * @return bool
176
     */
177
    protected function canEdit($player, $model)
178
    {
179
        return $player->canEdit($model);
180
    }
181
182
    /**
183
     * Get a redirection response to a model
184
     *
185
     * Goes to a list of models of the same type if the provided model does not
186
     * have a URL
187
     *
188
     * @param  ModelInterface $model The model to redirect to
189
     * @return Response
190
     */
191
    protected function redirectTo($model)
192
    {
193
        if ($model instanceof UrlModel) {
194
            return new RedirectResponse($model->getUrl());
195
        } else {
196
            return $this->redirectToList($model);
197
        }
198
    }
199
200
    /**
201
     * Get a redirection response to a list of models
202
     *
203
     * @param  ModelInterface $model The model to whose list we should redirect
204
     * @return Response
205
     */
206
    protected function redirectToList($model)
207
    {
208
        $route = $model->getRouteName('list');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface ModelInterface as the method getRouteName() does only exist in the following implementations of said interface: AliasModel, AvatarModel, Ban, Conversation, Invitation, Map, Match, News, NewsCategory, Page, Player, Server, Team, UrlModel.

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...
209
        $url = Service::getGenerator()->generate($route);
210
211
        return new RedirectResponse($url);
212
    }
213
214
    /**
215
     * Dynamically get the form to show to the user
216
     *
217
     * @param  \Model|null      $model The model being edited, `null` if we're creating one
218
     * @return ModelFormCreator
219
     */
220
    private function getFormCreator($model = null)
221
    {
222
        $type = ($model instanceof Model) ? $model->getType() : $this->getName();
223
        $type = ucfirst($type);
224
225
        $creatorClass = "\\BZIon\\Form\\Creator\\{$type}FormCreator";
226
        $creator = new $creatorClass($model, $this->getMe(), $this);
227
228
        return $creator;
229
    }
230
231
    /**
232
     * Get a message to show to the user
233
     * @param  \ModelInterface|string $model  The model (or type) to show a message for
234
     * @param  string                 $action The action that will be performed (softDelete, hardDelete, create or edit)
235
     * @param  string                 $status The message's status (confirm, error or success)
236
     * @return string
237
     */
238 1
    private function getMessage($model, $action, $status, $escape = true)
0 ignored issues
show
Unused Code introduced by
The parameter $escape is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
239
    {
240 1
        if ($model instanceof Model) {
241
            $type = strtolower($model->getTypeForHumans());
242
243
            if ($model instanceof NamedModel) {
244
                // Twig will not escape the message on confirmation forms
245
                $name = $model->getName();
246
                if ($status == 'confirm') {
247
                    $name = Model::escape($name);
248
                }
249
250
                $messages = $this->getMessages($type, $name);
251
252
                return $messages[$action][$status]['named'];
253
            } else {
254
                $messages = $this->getMessages($type);
255
256
                return $messages[$action][$status]['unnamed'];
257
            }
258
        } else {
259 1
            $messages = $this->getMessages(strtolower($model));
260
261 1
            return $messages[$action][$status];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $messages[$action][$status]; (array) is incompatible with the return type documented by CRUDController::getMessage of type string.

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...
262
        }
263
    }
264
265
    /**
266
     * Get a list of messages to show to the user
267
     * @param  string $type The type of the model that the message refers to
268
     * @param  string $name The name of the model
269
     * @return array
270
     */
271 1
    protected function getMessages($type, $name = '')
272
    {
273
        return array(
274
            'hardDelete' => array(
275
                'confirm' => array(
276
                    'named' => <<<"WARNING"
277 1
Are you sure you want to wipe <strong>$name</strong>?<br />
278
<strong><em>DANGER</em></strong>: This action will <strong>permanently</strong>
279 1
erase the $type from the database, including any objects directly related to it!
280
WARNING
281
                ,
282
                    'unnamed' => <<<"WARNING"
283
Are you sure you want to wipe this $type?<br />
284
<strong><em>DANGER</em></strong>: This action will <strong>permanently</strong>
285 1
erase the $type from the database, including any objects directly related to it!
286
WARNING
287
                ),
288
                'forbidden' => array(
289 1
                    'named'   => "You are not allowed to delete the $type $name",
290 1
                    'unnamed' => "You are not allowed to delete this $type",
291
                ),
292
                'success' => array(
293 1
                    'named'   => "The $type $name was permanently erased from the database",
294 1
                    'unnamed' => "The $type has been permanently erased from the database",
295
                ),
296
            ),
297
            'softDelete' => array(
298
                'confirm' => array(
299 1
                    'named'   => "Are you sure you want to delete <strong>$name</strong>?",
300 1
                    'unnamed' => "Are you sure you want to delete this $type?",
301
                ),
302
                'forbidden' => array(
303 1
                    'named'   => "You cannot delete the $type $name",
304 1
                    'unnamed' => "You can't delete this $type",
305
                ),
306
                'success' => array(
307 1
                    'named'   => "The $type $name was deleted successfully",
308 1
                    'unnamed' => "The $type was deleted successfully",
309
                ),
310
            ),
311
            'edit' => array(
312
                'forbidden' => array(
313 1
                    'named'   => "You are not allowed to edit the $type $name",
314 1
                    'unnamed' => "You aren't allowed to edit this $type",
315
                ),
316
                'success' => array(
317 1
                    'named'   => "The $type $name has been successfully updated",
318 1
                    'unnamed' => "The $type was updated successfully",
319
                ),
320
            ),
321
            'create' => array(
322 1
                'forbidden' => "You are not allowed to create a new $type",
323
                'success'   => array(
324 1
                    'named'   => "The $type $name was created successfully",
325 1
                    'unnamed' => "The $type was created successfully",
326
                ),
327
            ),
328
        );
329
    }
330
}
331