Completed
Push — 0.3-dev ( 291149...449c2f )
by Arnaud
14:26
created

CRUDController   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 291
Duplicated Lines 11.34 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 7
Bugs 2 Features 0
Metric Value
wmc 21
c 7
b 2
f 0
lcom 1
cbo 15
dl 33
loc 291
rs 9.1666

8 Methods

Rating   Name   Duplication   Size   Complexity  
B batchAction() 0 26 3
B createAction() 18 33 3
B editAction() 15 32 3
A deleteAction() 0 23 2
B exportEntities() 0 42 5
B listAction() 0 41 3
A forward404IfNotAllowed() 0 18 1
A getAdminFromRequest() 0 6 1

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
namespace LAG\AdminBundle\Controller;
4
5
use LAG\AdminBundle\Admin\AdminInterface;
6
use LAG\AdminBundle\Form\Handler\ListFormHandler;
7
use LAG\AdminBundle\Form\Type\AdminListType;
8
use LAG\AdminBundle\Form\Type\BatchActionType;
9
use BlueBear\BaseBundle\Behavior\ControllerTrait;
10
use DateTime;
11
use Doctrine\ORM\Mapping\MappingException;
12
use EE\DataExporterBundle\Service\DataExporter;
13
use Exception;
14
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
15
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
16
use Symfony\Component\HttpFoundation\RedirectResponse;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\Response;
19
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20
use Symfony\Component\PropertyAccess\PropertyAccess;
21
use Symfony\Component\Security\Core\Role\Role;
22
23
/**
24
 * Class CRUDController
25
 *
26
 * Generic CRUD controller
27
 */
28
class CRUDController extends Controller
29
{
30
    use ControllerTrait;
31
32
    /**
33
     * Generic list action
34
     *
35
     * @Template("LAGAdminBundle:CRUD:list.html.twig")
36
     * @param Request $request
37
     * @return array
38
     */
39
    public function listAction(Request $request)
40
    {
41
        // retrieve admin from request route parameters
42
        $admin = $this->getAdminFromRequest($request);
43
        $admin->handleRequest($request, $this->getUser());
44
        // creating list form
45
        $form = $this->createForm(AdminListType::class, [
46
            'entities' => $admin->getEntities()
47
        ], [
48
            'batch_actions' => $admin
49
                ->getCurrentAction()
50
                ->getBatchActions()
51
        ]);
52
        $form->handleRequest($request);
53
54
        if ($request->get('export')) {
55
            return $this->exportEntities($admin, $request->get('export'));
56
        }
57
        if ($form->isValid()) {
58
            // get ids and batch action from list form data
59
            $formHandler = new ListFormHandler();
60
            $data = $formHandler->handle($form);
61
            $batchForm = $this->createForm(BatchActionType::class, [
62
                'batch_action' => $data['batch_action'],
63
                'entity_ids' => $data['ids']
64
            ], [
65
                'labels' => $data['labels']
66
            ]);
67
68
            // render batch view
69
            return $this->render('LAGAdminBundle:CRUD:batch.html.twig', [
70
                'admin' => $admin,
71
                'form' => $batchForm->createView()
72
            ]);
73
        }
74
        return [
75
            'admin' => $admin,
76
            'action' => $admin->getCurrentAction(),
77
            'form' => $form->createView()
78
        ];
79
    }
80
81
    /**
82
     * @param Request $request
83
     * @return RedirectResponse
84
     */
85
    public function batchAction(Request $request)
86
    {
87
        $admin = $this->getAdminFromRequest($request);
88
        $admin->handleRequest($request, $this->getUser());
89
        // create batch action form
90
        $form = $this->createForm(BatchActionType::class, [
91
            'batch_action' => [],
92
            'entity_ids' => []
93
        ]);
94
        $form->handleRequest($request);
95
96
        if ($form->isValid()) {
97
            $data = $form->getData();
98
            $admin->load([
99
                'id' => $data['entity_ids']
100
            ]);
101
102
            if ($data['batch_action'] == 'delete') {
103
                $admin->remove();
104
            }
105
        } else {
106
            throw new NotFoundHttpException('Invalid batch parameters');
107
        }
108
        // redirect to list view
109
        return $this->redirectToRoute($admin->generateRouteName('list'));
110
    }
111
112
    /**
113
     * Generic create action
114
     *
115
     * @Template("LAGAdminBundle:CRUD:edit.html.twig")
116
     * @param Request $request
117
     * @return array
118
     */
119
    public function createAction(Request $request)
120
    {
121
        $admin = $this->getAdminFromRequest($request);
122
        $admin->handleRequest($request, $this->getUser());
123
        // check permissions
124
        $this->forward404IfNotAllowed($admin);
125
        // create form
126
        $form = $this->createForm($admin->getConfiguration()->getFormType());
127
        $form->handleRequest($request);
128
129 View Code Duplication
        if ($form->isValid()) {
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...
130
            // save entity
131
            $admin->save();
132
133
            // if save is pressed, user stay on the edit view
134
            if ($request->request->get('submit') == 'save') {
135
                $editRoute = $admin->generateRouteName('edit');
136
137
                return $this->redirectToRoute($editRoute, [
138
                    'id' => $admin->getUniqueEntity()->getId(),
139
                ]);
140
            } else {
141
                // otherwise user is redirected to list view
142
                $listRoute = $admin->generateRouteName('list');
143
144
                return $this->redirectToRoute($listRoute);
145
            }
146
        }
147
        return [
148
            'admin' => $admin,
149
            'form' => $form->createView(),
150
        ];
151
    }
152
153
    /**
154
     * Generic edit action.
155
     *
156
     * @Template("LAGAdminBundle:CRUD:edit.html.twig")
157
     *
158
     * @param Request $request
159
     *
160
     * @return array|RedirectResponse
161
     */
162
    public function editAction(Request $request)
163
    {
164
        $admin = $this->getAdminFromRequest($request);
165
        $admin->handleRequest($request, $this->getUser());
166
        // check permissions
167
        $this->forward404IfNotAllowed($admin);
168
        // create form
169
        $form = $this->createForm($admin->getConfiguration()->getFormType(), $admin->getUniqueEntity());
170
        $form->handleRequest($request);
171
        $accessor = PropertyAccess::createPropertyAccessor();
172
173 View Code Duplication
        if ($form->isValid()) {
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...
174
            $admin->save();
175
176
            if ($request->request->get('submit') == 'save') {
177
                $saveRoute = $admin->generateRouteName('edit');
178
179
                return $this->redirectToRoute($saveRoute, [
180
                    'id' => $accessor->getValue($admin->getUniqueEntity(), 'id'),
181
                ]);
182
            } else {
183
                $listRoute = $admin->generateRouteName('list');
184
                // redirect to list
185
                return $this->redirectToRoute($listRoute);
186
            }
187
        }
188
189
        return [
190
            'admin' => $admin,
191
            'form' => $form->createView(),
192
        ];
193
    }
194
195
    /**
196
     * Generic delete action
197
     *
198
     * @Template("LAGAdminBundle:CRUD:delete.html.twig")
199
     *
200
     * @param Request $request
201
     *
202
     * @return RedirectResponse
203
     */
204
    public function deleteAction(Request $request)
205
    {
206
        $admin = $this->getAdminFromRequest($request);
207
        $admin->handleRequest($request, $this->getUser());
208
        // check permissions
209
        $this->forward404IfNotAllowed($admin);
210
        // create form to avoid deletion by url
211
        $form = $this->createForm('delete', $admin->getUniqueEntity());
212
        $form->handleRequest($request);
213
214
        if ($form->isValid()) {
215
            $admin->remove();
216
            // redirect to list
217
            $listRoute = $admin->generateRouteName('list');
218
219
            return $this->redirectToRoute($listRoute);
220
        }
221
222
        return [
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('admin' => ...> $form->createView()); (array<string,LAG\AdminBu...omponent\Form\FormView>) is incompatible with the return type documented by LAG\AdminBundle\Controll...ontroller::deleteAction of type Symfony\Component\HttpFoundation\RedirectResponse.

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...
223
            'admin' => $admin,
224
            'form' => $form->createView(),
225
        ];
226
    }
227
228
    /**
229
     * Export entities according to a type (json, csv, xls...)
230
     *
231
     * @param AdminInterface $admin
232
     * @param $exportType
233
     *
234
     * @return Response
235
     *
236
     * @throws MappingException
237
     */
238
    protected function exportEntities(AdminInterface $admin, $exportType)
239
    {
240
        // check allowed export types
241
        $this->forward404Unless(in_array($exportType, ['json', 'html', 'xls', 'csv', 'xml']));
242
        /** @var DataExporter $exporter */
243
        $exporter = $this->get('ee.dataexporter');
244
        $metadata = $this
245
            ->getEntityManager()
246
            ->getClassMetadata($admin->getConfiguration()->getEntityName());
247
        $exportColumns = [];
248
        $fields = $metadata->getFieldNames();
249
        $hooks = [];
250
251
        foreach ($fields as $fieldName) {
252
            $exporter->addHook(function ($fieldValue) {
253
                // if field is an array
254
                if (is_array($fieldValue)) {
255
                    $value = recursiveImplode(', ', $fieldValue);
256
                } elseif ($fieldValue instanceof DateTime) {
257
                    // format date in string
258
                    $value = $fieldValue->format('c');
259
                } else {
260
                    $value = $fieldValue;
261
                }
262
263
                return $value;
264
            }, "{$fieldName}");
265
            // add field column to export
266
            $exportColumns[$fieldName] = $fieldName;
267
        }
268
        $exporter
269
            ->setOptions($exportType, [
270
                'fileName' => $admin->getName() . '-export-' . date('Y-m-d'),
271
            ])
272
            ->setColumns($exportColumns)
273
            ->setData($admin->getEntities());
274
        foreach ($hooks as $hookName => $hook) {
275
            $exporter->addHook($hook, $hookName);
276
        }
277
278
        return $exporter->render();
279
    }
280
281
    /**
282
     * Forward to 404 if user is not allowed by configuration for an action.
283
     *
284
     * @param AdminInterface $admin
285
     */
286
    protected function forward404IfNotAllowed(AdminInterface $admin)
287
    {
288
        // TODO move authorizations logic into kernel.request event
289
        $this->forward404Unless($this->getUser(), 'You must be logged to access to this url');
290
        $roles = $this
291
            ->getUser()
292
            ->getRoles();
293
        // check permissions and actions
294
        $this->forward404Unless(
295
            $admin->isActionGranted($admin->getCurrentAction()->getName(), $roles),
296
            sprintf('User with roles %s not allowed for action "%s"',
297
                implode(', ', array_map(function (Role $role) {
298
                    return $role->getRole();
299
                }, $roles)),
300
                $admin->getCurrentAction()->getName()
301
            )
302
        );
303
    }
304
305
    /**
306
     * Return an Admin object according to the request route parameters.
307
     *
308
     * @param Request $request
309
     * @return AdminInterface
310
     * @throws Exception
311
     */
312
    protected function getAdminFromRequest(Request $request)
313
    {
314
        return $this
315
            ->get('lag.admin.factory')
316
            ->getAdminFromRequest($request);
317
    }
318
}
319