Completed
Push — fm-matches ( f867e2 )
by Vladimir
16:54
created

CRUDController::create()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.0061

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 30
ccs 17
cts 18
cp 0.9444
rs 8.439
cc 6
eloc 18
nc 6
nop 2
crap 6.0061
1
<?php
2
3
use BZIon\Form\Creator\ModelFormCreator;
4
use Symfony\Component\Form\Form;
5
use Symfony\Component\HttpFoundation\RedirectResponse;
6
use Symfony\Component\HttpFoundation\Response;
7
8
/**
9
 * A controller with actions for creating, reading, updating and deleting models
10
 * @package BZiON\Controllers
11
 */
12
abstract class CRUDController extends JSONController
13
{
14
    /**
15
     * Make sure that the data of a form is valid, only called when creating a
16
     * new object
17
     * @param  Form $form The submitted form
18
     * @return void
19
     */
20 1
    protected function validateNew($form)
21
    {
22 1
    }
23
24
    /**
25
     * Make sure that the data of a form is valid
26
     * @param  Form $form The submitted form
27
     * @return void
28
     */
29
    protected function validate($form)
30
    {
31
    }
32
33
    /**
34
     * Delete a model
35
     * @param  PermissionModel    $model     The model we want to delete
36
     * @param  Player             $me        The user who wants to delete the model
37
     * @param  Closure|null       $onSuccess Something to do when the model is deleted
38
     * @throws ForbiddenException
39
     * @return mixed              The response to show to the user
40
     */
41 1
    protected function delete(PermissionModel $model, Player $me, $onSuccess = null)
42
    {
43 1
        if ($model->isDeleted()) {
44
            // We will have to hard delete the model
45
            $hard = true;
46
            $message = 'hardDelete';
47
            $action = 'Erase forever';
48
        } else {
49 1
            $hard = false;
50 1
            $message = 'softDelete';
51 1
            $action = 'Delete';
52
        }
53
54 1
        if (!$this->canDelete($me, $model, $hard)) {
55
            throw new ForbiddenException($this->getMessage($model, $message, 'forbidden'));
56
        }
57
58 1
        $successMessage = $this->getMessage($model, $message, 'success');
59 1
        $redirection    = $this->redirectToList($model);
60
61 1
        return $this->showConfirmationForm(function () use ($model, $hard, $redirection, $onSuccess) {
62 1
            if ($hard) {
63
                $model->wipe();
64
            } else {
65 1
                $model->delete();
66
            }
67
68 1
            if ($onSuccess) {
69 1
                $response = $onSuccess();
70 1
                if ($response instanceof Response) {
71
                    return $response;
72
                }
73
            }
74
75 1
            return $redirection;
76 1
        }, $this->getMessage($model, $message, 'confirm'), $successMessage, $action);
77
    }
78
79
    /**
80
     * Create a model
81
     *
82
     * This method requires that you have implemented enter() and a form creator
83
     * for the model
84
     *
85
     * @param  Player             $me The user who wants to create the model
86
     * @param  Closure|null       $onSuccess The function to call on success
87
     * @throws ForbiddenException
88
     * @return mixed              The response to show to the user
89
     */
90 1
    protected function create(Player $me, $onSuccess = null)
91
    {
92 1
        if (!$this->canCreate($me)) {
93 1
            throw new ForbiddenException($this->getMessage($this->getName(), 'create', 'forbidden'));
94
        }
95
96 1
        $creator = $this->getFormCreator();
97 1
        $form = $creator->create()->handleRequest($this->getRequest());
98
99 1
        if ($form->isSubmitted()) {
100 1
            $this->validate($form);
101 1
            $this->validateNew($form);
102 1
            if ($form->isValid()) {
103 1
                $model = $creator->enter($form);
104 1
                $this->getFlashBag()->add("success",
105 1
                     $this->getMessage($model, 'create', 'success'));
106
107 1
                if ($onSuccess) {
108 1
                    $response = $onSuccess($model);
109 1
                    if ($response instanceof Response) {
110
                        return $response;
111
                    }
112
                }
113
114 1
                return $this->redirectTo($model);
115
            }
116
        }
117
118 1
        return array("form" => $form->createView());
119
    }
120
121
    /**
122
     * Edit a model
123
     *
124
     * This method requires that you have implemented update() and a form creator
125
     * for the model
126
     *
127
     * @param  PermissionModel    $model The model we want to edit
128
     * @param  Player             $me    The user who wants to edit the model
129
     * @param  string             $type  The name of the variable to pass to the view
130
     * @throws ForbiddenException
131
     * @return mixed              The response to show to the user
132
     */
133
    protected function edit(PermissionModel $model, Player $me, $type)
134
    {
135
        if (!$this->canEdit($me, $model)) {
136
            throw new ForbiddenException($this->getMessage($model, 'edit', 'forbidden'));
137
        }
138
139
        $creator = $this->getFormCreator($model);
140
        $form = $creator->create()->handleRequest($this->getRequest());
141
142
        if ($form->isSubmitted()) {
143
            $this->validate($form);
144
            if ($form->isValid()) {
145
                $creator->update($form, $model);
146
                $this->getFlashBag()->add("success",
147
                    $this->getMessage($model, 'edit', 'success'));
148
149
                return $this->redirectTo($model);
150
            }
151
        }
152
153
        return array("form" => $form->createView(), $type => $model);
154
    }
155
156
    /**
157
     * Find whether a player can delete a model
158
     *
159
     * @param  Player          $player The player who wants to delete the model
160
     * @param  PermissionModel $model  The model that will be deleted
161
     * @param  bool         $hard   Whether to hard-delete the model instead of soft-deleting it
162
     * @return bool
163
     */
164 1
    protected function canDelete($player, $model, $hard = false)
165
    {
166 1
        return $player->canDelete($model, $hard);
167
    }
168
169
    /**
170
     * Find whether a player can create a model
171
     *
172
     * @param  Player  $player The player who wants to create a model
173
     * @return bool
174
     */
175 1
    protected function canCreate($player)
176
    {
177 1
        $modelName = $this->getName();
178
179 1
        return $player->canCreate($modelName);
180
    }
181
182
    /**
183
     * Find whether a player can edit a model
184
     *
185
     * @param  Player          $player The player who wants to delete the model
186
     * @param  PermissionModel $model  The model which will be edited
187
     * @return bool
188
     */
189
    protected function canEdit($player, $model)
190
    {
191
        return $player->canEdit($model);
192
    }
193
194
    /**
195
     * Get a redirection response to a model
196
     *
197
     * Goes to a list of models of the same type if the provided model does not
198
     * have a URL
199
     *
200
     * @param  ModelInterface $model The model to redirect to
201
     * @return Response
202
     */
203 1
    protected function redirectTo($model)
204
    {
205 1
        if ($model instanceof UrlModel) {
206 1
            return new RedirectResponse($model->getUrl());
207
        } else {
208
            return $this->redirectToList($model);
209
        }
210
    }
211
212
    /**
213
     * Get a redirection response to a list of models
214
     *
215
     * @param  ModelInterface $model The model to whose list we should redirect
216
     * @return Response
217
     */
218 1
    protected function redirectToList($model)
219
    {
220 1
        $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, Role, 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...
221 1
        $url = Service::getGenerator()->generate($route);
222
223 1
        return new RedirectResponse($url);
224
    }
225
226
    /**
227
     * Dynamically get the form to show to the user
228
     *
229
     * @param  \Model|null      $model The model being edited, `null` if we're creating one
230
     * @return ModelFormCreator
231
     */
232 1
    private function getFormCreator($model = null)
233
    {
234 1
        $type = ($model instanceof Model) ? $model->getType() : $this->getName();
235 1
        $type = ucfirst($type);
236
237 1
        $creatorClass = "\\BZIon\\Form\\Creator\\{$type}FormCreator";
238 1
        $creator = new $creatorClass($model, $this->getMe(), $this);
239
240 1
        return $creator;
241
    }
242
243
    /**
244
     * Get a message to show to the user
245
     * @param  \ModelInterface|string $model  The model (or type) to show a message for
246
     * @param  string                 $action The action that will be performed (softDelete, hardDelete, create or edit)
247
     * @param  string                 $status The message's status (confirm, error or success)
248
     * @return string
249
     */
250 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...
251
    {
252 1
        if ($model instanceof Model) {
253 1
            $type = strtolower($model->getTypeForHumans());
254
255 1
            if ($model instanceof NamedModel) {
256
                // Twig will not escape the message on confirmation forms
257 1
                $name = $model->getName();
258 1
                if ($status == 'confirm') {
259 1
                    $name = Model::escape($name);
260
                }
261
262 1
                $messages = $this->getMessages($type, $name);
263
264 1
                return $messages[$action][$status]['named'];
265
            } else {
266
                $messages = $this->getMessages($type);
267
268
                return $messages[$action][$status]['unnamed'];
269
            }
270
        } else {
271 1
            $messages = $this->getMessages(strtolower($model));
272
273 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...
274
        }
275
    }
276
277
    /**
278
     * Get a list of messages to show to the user
279
     * @param  string $type The type of the model that the message refers to
280
     * @param  string $name The name of the model
281
     * @return array
282
     */
283 1
    protected function getMessages($type, $name = '')
284
    {
285
        return array(
286
            'hardDelete' => array(
287
                'confirm' => array(
288
                    'named' => <<<"WARNING"
289 1
Are you sure you want to wipe <strong>$name</strong>?<br />
290
<strong><em>DANGER</em></strong>: This action will <strong>permanently</strong>
291 1
erase the $type from the database, including any objects directly related to it!
292
WARNING
293
                ,
294
                    'unnamed' => <<<"WARNING"
295 1
Are you sure you want to wipe this $type?<br />
296
<strong><em>DANGER</em></strong>: This action will <strong>permanently</strong>
297 1
erase the $type from the database, including any objects directly related to it!
298
WARNING
299
                ),
300
                'forbidden' => array(
301 1
                    'named'   => "You are not allowed to delete the $type $name",
302 1
                    'unnamed' => "You are not allowed to delete this $type",
303
                ),
304
                'success' => array(
305 1
                    'named'   => "The $type $name was permanently erased from the database",
306 1
                    'unnamed' => "The $type has been permanently erased from the database",
307
                ),
308
            ),
309
            'softDelete' => array(
310
                'confirm' => array(
311 1
                    'named'   => "Are you sure you want to delete <strong>$name</strong>?",
312 1
                    'unnamed' => "Are you sure you want to delete this $type?",
313
                ),
314
                'forbidden' => array(
315 1
                    'named'   => "You are not allowed to delete the $type $name",
316 1
                    'unnamed' => "You aren't allowed to delete this $type",
317
                ),
318
                'success' => array(
319 1
                    'named'   => "The $type $name was deleted successfully",
320 1
                    'unnamed' => "The $type was deleted successfully",
321
                ),
322
            ),
323
            'edit' => array(
324
                'forbidden' => array(
325 1
                    'named'   => "You are not allowed to edit the $type $name",
326 1
                    'unnamed' => "You aren't allowed to edit this $type",
327
                ),
328
                'success' => array(
329 1
                    'named'   => "The $type $name has been successfully updated",
330 1
                    'unnamed' => "The $type was updated successfully",
331
                ),
332
            ),
333
            'create' => array(
334 1
                'forbidden' => "You are not allowed to create a new $type",
335
                'success'   => array(
336 1
                    'named'   => "The $type $name was created successfully",
337 1
                    'unnamed' => "The $type was created successfully",
338
                ),
339
            ),
340
        );
341
    }
342
}
343