1
|
|
|
<?php |
2
|
|
|
namespace ZfcDatagrid; |
3
|
|
|
|
4
|
|
|
use ArrayIterator; |
5
|
|
|
use Doctrine\Common\Collections\Collection; |
6
|
|
|
use Doctrine\ORM\QueryBuilder; |
7
|
|
|
use Zend\Cache; |
8
|
|
|
use Zend\Console\Request as ConsoleRequest; |
9
|
|
|
use Zend\Db\Sql\Select as ZendSelect; |
10
|
|
|
use Zend\Http\PhpEnvironment\Request as HttpRequest; |
11
|
|
|
use Zend\I18n\Translator\Translator; |
12
|
|
|
use Zend\Mvc\MvcEvent; |
13
|
|
|
use Zend\Paginator\Paginator; |
14
|
|
|
use Zend\Session\Container as SessionContainer; |
15
|
|
|
use Zend\Stdlib\ResponseInterface; |
16
|
|
|
use Zend\View\Model\JsonModel; |
17
|
|
|
use Zend\View\Model\ViewModel; |
18
|
|
|
use ZfcDatagrid\Column\Style; |
19
|
|
|
|
20
|
|
|
class Datagrid |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* |
24
|
|
|
* @var array |
25
|
|
|
*/ |
26
|
|
|
protected $options = []; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* |
30
|
|
|
* @var SessionContainer |
31
|
|
|
*/ |
32
|
|
|
protected $session; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* |
36
|
|
|
* @var Cache\Storage\StorageInterface |
37
|
|
|
*/ |
38
|
|
|
protected $cache; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* |
42
|
|
|
* @var string |
43
|
|
|
*/ |
44
|
|
|
protected $cacheId; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* |
48
|
|
|
* @var MvcEvent |
49
|
|
|
*/ |
50
|
|
|
protected $mvcEvent; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
protected $parameters = []; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* |
60
|
|
|
* @var mixed |
61
|
|
|
*/ |
62
|
|
|
protected $url; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* |
66
|
|
|
* @var HttpRequest |
67
|
|
|
*/ |
68
|
|
|
protected $request; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* View or Response |
72
|
|
|
* |
73
|
|
|
* @var \Zend\Http\Response\Stream|\Zend\View\Model\ViewModel |
74
|
|
|
*/ |
75
|
|
|
protected $response; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* |
79
|
|
|
* @var Renderer\AbstractRenderer |
80
|
|
|
*/ |
81
|
|
|
private $renderer; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* |
85
|
|
|
* @var Translator |
86
|
|
|
*/ |
87
|
|
|
protected $translator; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* |
91
|
|
|
* @var string |
92
|
|
|
*/ |
93
|
|
|
protected $id; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* The grid title |
97
|
|
|
* |
98
|
|
|
* @var string |
99
|
|
|
*/ |
100
|
|
|
protected $title = ''; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* |
104
|
|
|
* @var DataSource\DataSourceInterface |
105
|
|
|
*/ |
106
|
|
|
protected $dataSource = null; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* |
110
|
|
|
* @var integer |
111
|
|
|
*/ |
112
|
|
|
protected $defaulItemsPerPage = 25; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* |
116
|
|
|
* @var array |
117
|
|
|
*/ |
118
|
|
|
protected $columns = []; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* |
122
|
|
|
* @var Style\AbstractStyle[] |
123
|
|
|
*/ |
124
|
|
|
protected $rowStyles = []; |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* |
128
|
|
|
* @var Column\Action\AbstractAction |
129
|
|
|
*/ |
130
|
|
|
protected $rowClickAction; |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* |
134
|
|
|
* @var Action\Mass |
135
|
|
|
*/ |
136
|
|
|
protected $massActions = []; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* The prepared data |
140
|
|
|
* |
141
|
|
|
* @var array |
142
|
|
|
*/ |
143
|
|
|
protected $preparedData = []; |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* |
147
|
|
|
* @var array |
148
|
|
|
*/ |
149
|
|
|
protected $isUserFilterEnabled = true; |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* |
153
|
|
|
* @var Paginator |
154
|
|
|
*/ |
155
|
|
|
protected $paginator = null; |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* |
159
|
|
|
* @var array |
160
|
|
|
*/ |
161
|
|
|
protected $exportRenderers; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @var string|null |
165
|
|
|
*/ |
166
|
|
|
protected $toolbarTemplate; |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* |
170
|
|
|
* @var array |
171
|
|
|
*/ |
172
|
|
|
protected $toolbarTemplateVariables = []; |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* |
176
|
|
|
* @var ViewModel |
177
|
|
|
*/ |
178
|
|
|
protected $viewModel; |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* |
182
|
|
|
* @var boolean |
183
|
|
|
*/ |
184
|
|
|
protected $isInit = false; |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* |
188
|
|
|
* @var boolean |
189
|
|
|
*/ |
190
|
|
|
protected $isDataLoaded = false; |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* |
194
|
|
|
* @var boolean |
195
|
|
|
*/ |
196
|
|
|
protected $isRendered = false; |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* |
200
|
|
|
* @var string |
201
|
|
|
*/ |
202
|
|
|
protected $forceRenderer; |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* |
206
|
|
|
* @var Renderer\AbstractRenderer |
207
|
|
|
*/ |
208
|
|
|
private $rendererService; |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @var array |
212
|
|
|
*/ |
213
|
|
|
private $specialMethods = [ |
214
|
|
|
'filterSelectOptions', |
215
|
|
|
'rendererParameter', |
216
|
|
|
'replaceValues', |
217
|
|
|
'select', |
218
|
|
|
'sortDefault', |
219
|
|
|
]; |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Init method is called automatically with the service creation |
223
|
|
|
*/ |
224
|
|
|
public function init() |
225
|
|
|
{ |
226
|
|
|
if ($this->getCache() === null) { |
227
|
|
|
$options = $this->getOptions(); |
228
|
|
|
$this->setCache(Cache\StorageFactory::factory($options['cache'])); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$this->isInit = true; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* |
236
|
|
|
* @return boolean |
237
|
|
|
*/ |
238
|
|
|
public function isInit() |
239
|
|
|
{ |
240
|
|
|
return (bool) $this->isInit; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Set the options from config |
245
|
|
|
* |
246
|
|
|
* @param array $config |
247
|
|
|
*/ |
248
|
|
|
public function setOptions(array $config) |
249
|
|
|
{ |
250
|
|
|
$this->options = $config; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Get the config options |
255
|
|
|
* |
256
|
|
|
* @return array |
257
|
|
|
*/ |
258
|
|
|
public function getOptions() |
259
|
|
|
{ |
260
|
|
|
return $this->options; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Set the grid id |
265
|
|
|
* |
266
|
|
|
* @param string $id |
267
|
|
|
*/ |
268
|
|
|
public function setId($id = null) |
269
|
|
|
{ |
270
|
|
|
if ($id !== null) { |
271
|
|
|
$id = preg_replace("/[^a-z0-9_\\\d]/i", '_', $id); |
272
|
|
|
|
273
|
|
|
$this->id = (string) $id; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Get the grid id |
279
|
|
|
* |
280
|
|
|
* @return string |
281
|
|
|
*/ |
282
|
|
|
public function getId() |
283
|
|
|
{ |
284
|
|
|
if (null === $this->id) { |
285
|
|
|
$this->id = 'defaultGrid'; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
return $this->id; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Set the session |
293
|
|
|
* |
294
|
|
|
* @param \Zend\Session\Container $session |
295
|
|
|
*/ |
296
|
|
|
public function setSession(SessionContainer $session) |
297
|
|
|
{ |
298
|
|
|
$this->session = $session; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Get session container |
303
|
|
|
* |
304
|
|
|
* Instantiate session container if none currently exists |
305
|
|
|
* |
306
|
|
|
* @return SessionContainer |
307
|
|
|
*/ |
308
|
|
|
public function getSession() |
309
|
|
|
{ |
310
|
|
|
if (null === $this->session) { |
311
|
|
|
// Using fully qualified name, to ensure polyfill class alias is used |
312
|
|
|
$this->session = new SessionContainer($this->getId()); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return $this->session; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @param Cache\Storage\StorageInterface $cache |
320
|
|
|
*/ |
321
|
|
|
public function setCache(Cache\Storage\StorageInterface $cache) |
322
|
|
|
{ |
323
|
|
|
$this->cache = $cache; |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* |
328
|
|
|
* @return Cache\Storage\StorageInterface |
329
|
|
|
*/ |
330
|
|
|
public function getCache() |
331
|
|
|
{ |
332
|
|
|
return $this->cache; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Set the cache id |
337
|
|
|
* |
338
|
|
|
* @param string $id |
339
|
|
|
*/ |
340
|
|
|
public function setCacheId($id) |
341
|
|
|
{ |
342
|
|
|
$this->cacheId = (string) $id; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* Get the cache id |
347
|
|
|
* |
348
|
|
|
* @return string |
349
|
|
|
*/ |
350
|
|
|
public function getCacheId() |
351
|
|
|
{ |
352
|
|
|
if (null === $this->cacheId) { |
353
|
|
|
$this->cacheId = md5($this->getSession() |
354
|
|
|
->getManager() |
355
|
|
|
->getId() . '_' . $this->getId()); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
return $this->cacheId; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @param MvcEvent $mvcEvent |
363
|
|
|
*/ |
364
|
|
|
public function setMvcEvent(MvcEvent $mvcEvent) |
365
|
|
|
{ |
366
|
|
|
$this->mvcEvent = $mvcEvent; |
367
|
|
|
$this->request = $mvcEvent->getRequest(); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* |
372
|
|
|
* @return MvcEvent |
373
|
|
|
*/ |
374
|
|
|
public function getMvcEvent() |
375
|
|
|
{ |
376
|
|
|
return $this->mvcEvent; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* |
381
|
|
|
* @return HttpRequest |
382
|
|
|
*/ |
383
|
|
|
public function getRequest() |
384
|
|
|
{ |
385
|
|
|
return $this->request; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Set the translator |
390
|
|
|
* |
391
|
|
|
* @param Translator $translator |
392
|
|
|
* @throws \InvalidArgumentException |
393
|
|
|
*/ |
394
|
|
View Code Duplication |
public function setTranslator($translator = null) |
|
|
|
|
395
|
|
|
{ |
396
|
|
|
if (! $translator instanceof Translator && ! $translator instanceof \Zend\I18n\Translator\TranslatorInterface) { |
397
|
|
|
throw new \InvalidArgumentException('Translator must be an instanceof "Zend\I18n\Translator\Translator" or "Zend\I18n\Translator\TranslatorInterface"'); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
$this->translator = $translator; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* |
405
|
|
|
* @return Translator |
406
|
|
|
*/ |
407
|
|
|
public function getTranslator() |
408
|
|
|
{ |
409
|
|
|
return $this->translator; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* |
414
|
|
|
* @return boolean |
415
|
|
|
*/ |
416
|
|
|
public function hasTranslator() |
417
|
|
|
{ |
418
|
|
|
if ($this->translator !== null) { |
419
|
|
|
return true; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
return false; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Set the data source |
427
|
|
|
* |
428
|
|
|
* @param mixed $data |
429
|
|
|
* @throws \Exception |
430
|
|
|
*/ |
431
|
|
|
public function setDataSource($data) |
432
|
|
|
{ |
433
|
|
|
if ($data instanceof DataSource\DataSourceInterface) { |
434
|
|
|
$this->dataSource = $data; |
435
|
|
|
} elseif (is_array($data)) { |
436
|
|
|
$this->dataSource = new DataSource\PhpArray($data); |
437
|
|
|
} elseif ($data instanceof QueryBuilder) { |
438
|
|
|
$this->dataSource = new DataSource\Doctrine2($data); |
439
|
|
|
} elseif ($data instanceof ZendSelect) { |
440
|
|
|
$args = func_get_args(); |
441
|
|
|
if (count($args) === 1 || (! $args[1] instanceof \Zend\Db\Adapter\Adapter && ! $args[1] instanceof \Zend\Db\Sql\Sql)) { |
442
|
|
|
throw new \InvalidArgumentException('For "Zend\Db\Sql\Select" also a "Zend\Db\Adapter\Sql" or "Zend\Db\Sql\Sql" is needed.'); |
443
|
|
|
} |
444
|
|
|
$this->dataSource = new DataSource\ZendSelect($data); |
445
|
|
|
$this->dataSource->setAdapter($args[1]); |
446
|
|
|
} elseif ($data instanceof Collection) { |
447
|
|
|
$args = func_get_args(); |
448
|
|
|
if (count($args) === 1 || ! $args[1] instanceof \Doctrine\ORM\EntityManager) { |
449
|
|
|
throw new \InvalidArgumentException('If providing a Collection, also the Doctrine\ORM\EntityManager is needed as a second parameter'); |
450
|
|
|
} |
451
|
|
|
$this->dataSource = new DataSource\Doctrine2Collection($data); |
452
|
|
|
$this->dataSource->setEntityManager($args[1]); |
453
|
|
|
} else { |
454
|
|
|
throw new \InvalidArgumentException('$data must implement the interface ZfcDatagrid\DataSource\DataSourceInterface'); |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
/** |
459
|
|
|
* |
460
|
|
|
* @return \ZfcDatagrid\DataSource\DataSourceInterface |
461
|
|
|
*/ |
462
|
|
|
public function getDataSource() |
463
|
|
|
{ |
464
|
|
|
return $this->dataSource; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* Datasource defined? |
469
|
|
|
* |
470
|
|
|
* @return boolean |
471
|
|
|
*/ |
472
|
|
|
public function hasDataSource() |
473
|
|
|
{ |
474
|
|
|
if ($this->dataSource !== null) { |
475
|
|
|
return true; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
return false; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* Set default items per page (-1 for unlimited) |
483
|
|
|
* |
484
|
|
|
* @param integer $count |
485
|
|
|
*/ |
486
|
|
|
public function setDefaultItemsPerPage($count = 25) |
487
|
|
|
{ |
488
|
|
|
$this->defaulItemsPerPage = (int) $count; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
/** |
492
|
|
|
* |
493
|
|
|
* @return integer |
494
|
|
|
*/ |
495
|
|
|
public function getDefaultItemsPerPage() |
496
|
|
|
{ |
497
|
|
|
return (int) $this->defaulItemsPerPage; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* Set the title |
502
|
|
|
* |
503
|
|
|
* @param string $title |
504
|
|
|
*/ |
505
|
|
|
public function setTitle($title) |
506
|
|
|
{ |
507
|
|
|
$this->title = (string) $title; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
/** |
511
|
|
|
* |
512
|
|
|
* @return string |
513
|
|
|
*/ |
514
|
|
|
public function getTitle() |
515
|
|
|
{ |
516
|
|
|
return $this->title; |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
/** |
520
|
|
|
* Add a external parameter |
521
|
|
|
* |
522
|
|
|
* @param string $name |
523
|
|
|
* @param mixed $value |
524
|
|
|
*/ |
525
|
|
|
public function addParameter($name, $value) |
526
|
|
|
{ |
527
|
|
|
$this->parameters[$name] = $value; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* These parameters are handled to the view + over all grid actions |
532
|
|
|
* |
533
|
|
|
* @param array $parameters |
534
|
|
|
*/ |
535
|
|
|
public function setParameters(array $parameters) |
536
|
|
|
{ |
537
|
|
|
$this->parameters = $parameters; |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
/** |
541
|
|
|
* |
542
|
|
|
* @return array |
543
|
|
|
*/ |
544
|
|
|
public function getParameters() |
545
|
|
|
{ |
546
|
|
|
return $this->parameters; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** |
550
|
|
|
* Has parameters? |
551
|
|
|
* |
552
|
|
|
* @return boolean |
553
|
|
|
*/ |
554
|
|
|
public function hasParameters() |
555
|
|
|
{ |
556
|
|
|
return (bool) $this->getParameters(); |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
/** |
560
|
|
|
* Set the base url |
561
|
|
|
* |
562
|
|
|
* @param string $url |
563
|
|
|
*/ |
564
|
|
|
public function setUrl($url) |
565
|
|
|
{ |
566
|
|
|
$this->url = $url; |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
/** |
570
|
|
|
* |
571
|
|
|
* @return string |
572
|
|
|
*/ |
573
|
|
|
public function getUrl() |
574
|
|
|
{ |
575
|
|
|
return $this->url; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Set the export renderers (overwrite the config) |
580
|
|
|
* |
581
|
|
|
* @param array $renderers |
582
|
|
|
*/ |
583
|
|
|
public function setExportRenderers(array $renderers = []) |
584
|
|
|
{ |
585
|
|
|
$this->exportRenderers = $renderers; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* Get the export renderers |
590
|
|
|
* |
591
|
|
|
* @return array |
592
|
|
|
*/ |
593
|
|
|
public function getExportRenderers() |
594
|
|
|
{ |
595
|
|
|
if (null === $this->exportRenderers) { |
596
|
|
|
$options = $this->getOptions(); |
597
|
|
|
$this->exportRenderers = $options['settings']['export']['formats']; |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
return $this->exportRenderers; |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
/** |
604
|
|
|
* Create a column from array instanceof |
605
|
|
|
* |
606
|
|
|
* @param array $config |
607
|
|
|
* |
608
|
|
|
* @return Column\AbstractColumn |
609
|
|
|
*/ |
610
|
|
|
private function createColumn($config) |
611
|
|
|
{ |
612
|
|
|
if ($config instanceof Column\AbstractColumn) { |
613
|
|
|
return $config; |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
if (! is_array($config) && ! $config instanceof Column\AbstractColumn) { |
617
|
|
|
throw new \InvalidArgumentException('createColumn() supports only a config array or instanceof Column\AbstractColumn as a parameter'); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$colType = isset($config['colType']) ? $config['colType'] : 'Select'; |
621
|
|
|
if (class_exists($colType, true)) { |
622
|
|
|
$class = $colType; |
623
|
|
|
} elseif (class_exists('ZfcDatagrid\\Column\\' . $colType, true)) { |
624
|
|
|
$class = 'ZfcDatagrid\\Column\\' . $colType; |
625
|
|
|
} else { |
626
|
|
|
throw new \InvalidArgumentException(sprintf('Column type: "%s" not found!', $colType)); |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
if ('ZfcDatagrid\\Column\\Select' == $class) { |
630
|
|
|
if (! isset($config['select']['column'])) { |
631
|
|
|
throw new \InvalidArgumentException('For "ZfcDatagrid\Column\Select" the option select[column] must be defined!'); |
632
|
|
|
} |
633
|
|
|
$table = isset($config['select']['table']) ? $config['select']['table'] : null; |
634
|
|
|
|
635
|
|
|
$instance = new $class($config['select']['column'], $table); |
636
|
|
|
} else { |
637
|
|
|
$instance = new $class(); |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
foreach ($config as $key => $value) { |
641
|
|
|
$method = 'set' . ucfirst($key); |
642
|
|
|
if (method_exists($instance, $method)) { |
643
|
|
|
if (in_array($key, $this->specialMethods)) { |
644
|
|
|
if (! is_array($value)) { |
645
|
|
|
$value = [ |
646
|
|
|
$value, |
647
|
|
|
]; |
648
|
|
|
} |
649
|
|
|
call_user_func_array([ |
650
|
|
|
$instance, |
651
|
|
|
$method, |
652
|
|
|
], $value); |
653
|
|
|
} else { |
654
|
|
|
call_user_func([ |
655
|
|
|
$instance, |
656
|
|
|
$method, |
657
|
|
|
], $value); |
658
|
|
|
} |
659
|
|
|
} |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
return $instance; |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Set multiple columns by array (willoverwrite all existing) |
667
|
|
|
* |
668
|
|
|
* @param array $columns |
669
|
|
|
*/ |
670
|
|
|
public function setColumns(array $columns) |
671
|
|
|
{ |
672
|
|
|
$useColumns = []; |
673
|
|
|
|
674
|
|
|
foreach ($columns as $col) { |
675
|
|
|
$col = $this->createColumn($col); |
676
|
|
|
$useColumns[$col->getUniqueId()] = $col; |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
$this->columns = $useColumns; |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
/** |
683
|
|
|
* Add a column by array config or instanceof Column\AbstractColumn |
684
|
|
|
* |
685
|
|
|
* @param array|Column\AbstractColumn $col |
686
|
|
|
*/ |
687
|
|
|
public function addColumn($col) |
688
|
|
|
{ |
689
|
|
|
$col = $this->createColumn($col); |
|
|
|
|
690
|
|
|
$this->columns[$col->getUniqueId()] = $col; |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** |
694
|
|
|
* |
695
|
|
|
* @return \ZfcDatagrid\Column\AbstractColumn[] |
696
|
|
|
*/ |
697
|
|
|
public function getColumns() |
698
|
|
|
{ |
699
|
|
|
return $this->columns; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
/** |
703
|
|
|
* |
704
|
|
|
* @param string $id |
705
|
|
|
* @return Column\AbstractColumn null |
706
|
|
|
*/ |
707
|
|
|
public function getColumnByUniqueId($id) |
708
|
|
|
{ |
709
|
|
|
if (isset($this->columns[$id])) { |
710
|
|
|
return $this->columns[$id]; |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
return; |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
/** |
717
|
|
|
* |
718
|
|
|
* @param Style\AbstractStyle $style |
719
|
|
|
*/ |
720
|
|
|
public function addRowStyle(Style\AbstractStyle $style) |
721
|
|
|
{ |
722
|
|
|
$this->rowStyles[] = $style; |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
/** |
726
|
|
|
* |
727
|
|
|
* @return Style\AbstractStyle[] |
728
|
|
|
*/ |
729
|
|
|
public function getRowStyles() |
730
|
|
|
{ |
731
|
|
|
return $this->rowStyles; |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* |
736
|
|
|
* @return boolean |
737
|
|
|
*/ |
738
|
|
|
public function hasRowStyles() |
739
|
|
|
{ |
740
|
|
|
return (bool) $this->rowStyles; |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
/** |
744
|
|
|
* If disabled, the toolbar filter will not be shown to the user |
745
|
|
|
* |
746
|
|
|
* @param boolean $mode |
747
|
|
|
*/ |
748
|
|
|
public function setUserFilterDisabled($mode = true) |
749
|
|
|
{ |
750
|
|
|
$this->isUserFilterEnabled = (bool) ! $mode; |
|
|
|
|
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* |
755
|
|
|
* @return boolean |
756
|
|
|
*/ |
757
|
|
|
public function isUserFilterEnabled() |
758
|
|
|
{ |
759
|
|
|
return (bool) $this->isUserFilterEnabled; |
760
|
|
|
} |
761
|
|
|
|
762
|
|
|
/** |
763
|
|
|
* Set the row click action - identity will be automatically appended! |
764
|
|
|
* |
765
|
|
|
* @param Column\Action\AbstractAction $action |
766
|
|
|
*/ |
767
|
|
|
public function setRowClickAction(Column\Action\AbstractAction $action) |
768
|
|
|
{ |
769
|
|
|
$this->rowClickAction = $action; |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
/** |
773
|
|
|
* |
774
|
|
|
* @return null Column\Action\AbstractAction |
775
|
|
|
*/ |
776
|
|
|
public function getRowClickAction() |
777
|
|
|
{ |
778
|
|
|
return $this->rowClickAction; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
/** |
782
|
|
|
* |
783
|
|
|
* @return boolean |
784
|
|
|
*/ |
785
|
|
|
public function hasRowClickAction() |
786
|
|
|
{ |
787
|
|
|
if (is_object($this->rowClickAction)) { |
788
|
|
|
return true; |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
return false; |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* Add a mass action |
796
|
|
|
* |
797
|
|
|
* @param Action\Mass $action |
798
|
|
|
*/ |
799
|
|
|
public function addMassAction(Action\Mass $action) |
800
|
|
|
{ |
801
|
|
|
$this->massActions[] = $action; |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
/** |
805
|
|
|
* |
806
|
|
|
* @return Action\Mass[] |
807
|
|
|
*/ |
808
|
|
|
public function getMassActions() |
809
|
|
|
{ |
810
|
|
|
return $this->massActions; |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
/** |
814
|
|
|
* |
815
|
|
|
* @return boolean |
816
|
|
|
*/ |
817
|
|
|
public function hasMassAction() |
818
|
|
|
{ |
819
|
|
|
return (bool) $this->massActions; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
/** |
823
|
|
|
* Overwrite the render |
824
|
|
|
* F.x. |
825
|
|
|
* if you want to directly render a PDF |
826
|
|
|
* |
827
|
|
|
* @param string $name |
828
|
|
|
*/ |
829
|
|
|
public function setRendererName($name = null) |
830
|
|
|
{ |
831
|
|
|
$this->forceRenderer = $name; |
832
|
|
|
} |
833
|
|
|
|
834
|
|
|
/** |
835
|
|
|
* Get the current renderer name |
836
|
|
|
* |
837
|
|
|
* @return string |
838
|
|
|
*/ |
839
|
|
|
public function getRendererName() |
840
|
|
|
{ |
841
|
|
|
$options = $this->getOptions(); |
842
|
|
|
$parameterName = $options['generalParameterNames']['rendererType']; |
843
|
|
|
|
844
|
|
|
if ($this->forceRenderer !== null) { |
845
|
|
|
// A special renderer was given -> use is |
846
|
|
|
$rendererName = $this->forceRenderer; |
847
|
|
|
} else { |
848
|
|
|
// DEFAULT |
849
|
|
|
if ($this->getRequest() instanceof ConsoleRequest) { |
850
|
|
|
$rendererName = $options['settings']['default']['renderer']['console']; |
851
|
|
|
} else { |
852
|
|
|
$rendererName = $options['settings']['default']['renderer']['http']; |
853
|
|
|
} |
854
|
|
|
} |
855
|
|
|
|
856
|
|
|
// From request |
857
|
|
|
if ($this->getRequest() instanceof HttpRequest && $this->getRequest()->getQuery($parameterName) != '') { |
858
|
|
|
$rendererName = $this->getRequest()->getQuery($parameterName); |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
return $rendererName; |
862
|
|
|
} |
863
|
|
|
|
864
|
|
|
/** |
865
|
|
|
* Return the current renderer |
866
|
|
|
* |
867
|
|
|
* @throws \Exception |
868
|
|
|
* @return \ZfcDatagrid\Renderer\AbstractRenderer |
869
|
|
|
*/ |
870
|
|
|
public function getRenderer() |
871
|
|
|
{ |
872
|
|
|
if (null === $this->renderer) { |
873
|
|
|
if (isset($this->rendererService)) { |
874
|
|
|
$renderer = $this->rendererService; |
875
|
|
|
if (! $renderer instanceof Renderer\AbstractRenderer) { |
876
|
|
|
throw new \Exception('Renderer service must implement "ZfcDatagrid\Renderer\AbstractRenderer"'); |
877
|
|
|
} |
878
|
|
|
$renderer->setOptions($this->getOptions()); |
879
|
|
|
$renderer->setMvcEvent($this->getMvcEvent()); |
880
|
|
|
if ($this->getToolbarTemplate() !== null) { |
881
|
|
|
$renderer->setToolbarTemplate($this->getToolbarTemplate()); |
882
|
|
|
} |
883
|
|
|
$renderer->setToolbarTemplateVariables($this->getToolbarTemplateVariables()); |
884
|
|
|
$renderer->setViewModel($this->getViewModel()); |
885
|
|
|
if ($this->hasTranslator()) { |
886
|
|
|
$renderer->setTranslator($this->getTranslator()); |
887
|
|
|
} |
888
|
|
|
$renderer->setTitle($this->getTitle()); |
889
|
|
|
$renderer->setColumns($this->getColumns()); |
890
|
|
|
$renderer->setRowStyles($this->getRowStyles()); |
891
|
|
|
$renderer->setCache($this->getCache()); |
892
|
|
|
$renderer->setCacheId($this->getCacheId()); |
893
|
|
|
|
894
|
|
|
$this->renderer = $renderer; |
895
|
|
|
} else { |
896
|
|
|
throw new \Exception(sprintf('Renderer service was not found, please register it: "zfcDatagrid.renderer.%s"', $this->getRendererName())); |
897
|
|
|
} |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
return $this->renderer; |
901
|
|
|
} |
902
|
|
|
|
903
|
|
|
/** |
904
|
|
|
* |
905
|
|
|
* @return boolean |
906
|
|
|
*/ |
907
|
|
|
public function isDataLoaded() |
908
|
|
|
{ |
909
|
|
|
return (bool) $this->isDataLoaded; |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
/** |
913
|
|
|
* Load the data |
914
|
|
|
*/ |
915
|
|
|
public function loadData() |
916
|
|
|
{ |
917
|
|
|
if (true === $this->isDataLoaded) { |
918
|
|
|
return true; |
919
|
|
|
} |
920
|
|
|
|
921
|
|
|
if ($this->isInit() !== true) { |
922
|
|
|
throw new \Exception('The init() method has to be called, before you can call loadData()!'); |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
if ($this->hasDataSource() === false) { |
926
|
|
|
throw new \Exception('No datasource defined! Please call "setDataSource()" first"'); |
927
|
|
|
} |
928
|
|
|
|
929
|
|
|
/** |
930
|
|
|
* Apply cache |
931
|
|
|
*/ |
932
|
|
|
$renderer = $this->getRenderer(); |
933
|
|
|
|
934
|
|
|
/** |
935
|
|
|
* Step 1) Apply needed columns + filters + sort |
936
|
|
|
* - from Request (HTML View) -> and save in cache for export |
937
|
|
|
* - or from cache (Export PDF / Excel) -> same view like HTML (without LIMIT/Pagination) |
938
|
|
|
*/ |
939
|
|
|
{ |
940
|
|
|
/** |
941
|
|
|
* Step 1.1) Only select needed columns (performance) |
942
|
|
|
*/ |
943
|
|
|
$this->getDataSource()->setColumns($this->getColumns()); |
944
|
|
|
|
945
|
|
|
/** |
946
|
|
|
* Step 1.2) Sorting |
947
|
|
|
*/ |
948
|
|
|
foreach ($renderer->getSortConditions() as $condition) { |
949
|
|
|
$this->getDataSource()->addSortCondition($condition['column'], $condition['sortDirection']); |
950
|
|
|
} |
951
|
|
|
|
952
|
|
|
/** |
953
|
|
|
* Step 1.3) Filtering |
954
|
|
|
*/ |
955
|
|
|
foreach ($renderer->getFilters() as $filter) { |
956
|
|
|
$this->getDataSource()->addFilter($filter); |
957
|
|
|
} |
958
|
|
|
} |
959
|
|
|
|
960
|
|
|
/* |
961
|
|
|
* Step 2) Load the data (Paginator) |
962
|
|
|
*/ |
963
|
|
|
{ |
964
|
|
|
$this->getDataSource()->execute(); |
965
|
|
|
$paginatorAdapter = $this->getDataSource()->getPaginatorAdapter(); |
966
|
|
|
|
967
|
|
|
\Zend\Paginator\Paginator::setDefaultScrollingStyle('Sliding'); |
968
|
|
|
|
969
|
|
|
$this->paginator = new Paginator($paginatorAdapter); |
970
|
|
|
$this->paginator->setCurrentPageNumber($renderer->getCurrentPageNumber()); |
971
|
|
|
$this->paginator->setItemCountPerPage($renderer->getItemsPerPage($this->getDefaultItemsPerPage())); |
972
|
|
|
|
973
|
|
|
/* @var $currentItems \ArrayIterator */ |
974
|
|
|
$data = $this->paginator->getCurrentItems(); |
975
|
|
|
if (! is_array($data)) { |
976
|
|
|
if ($data instanceof \Zend\Db\ResultSet\ResultSet) { |
977
|
|
|
$data = $data->toArray(); |
978
|
|
|
} elseif ($data instanceof ArrayIterator) { |
979
|
|
|
$data = $data->getArrayCopy(); |
980
|
|
|
} else { |
981
|
|
|
if (is_object($data)) { |
982
|
|
|
$add = get_class($data); |
983
|
|
|
} else { |
984
|
|
|
$add = '[no object]'; |
985
|
|
|
} |
986
|
|
|
throw new \Exception( |
987
|
|
|
sprintf('The paginator returned an unknown result: %s (allowed: \ArrayIterator or a plain php array)', $add) |
988
|
|
|
); |
989
|
|
|
} |
990
|
|
|
} |
991
|
|
|
} |
992
|
|
|
|
993
|
|
|
/* |
994
|
|
|
* check if the export is enabled |
995
|
|
|
* Save cache |
996
|
|
|
*/ |
997
|
|
|
if ($this->getOptions()['settings']['export']['enabled'] && $renderer->isExport() === false) { |
998
|
|
|
$cacheData = [ |
999
|
|
|
'sortConditions' => $renderer->getSortConditions(), |
1000
|
|
|
'filters' => $renderer->getFilters(), |
1001
|
|
|
'currentPage' => $this->getPaginator()->getCurrentPageNumber(), |
1002
|
|
|
]; |
1003
|
|
|
$success = $this->getCache()->setItem($this->getCacheId(), $cacheData); |
1004
|
|
|
if ($success !== true) { |
1005
|
|
|
/** @var \Zend\Cache\Storage\Adapter\FilesystemOptions $options */ |
1006
|
|
|
$options = $this->getCache()->getOptions(); |
1007
|
|
|
throw new \Exception( |
1008
|
|
|
sprintf( |
1009
|
|
|
'Could not save the datagrid cache. Does the directory "%s" exists and is writeable? CacheId: %s', |
1010
|
|
|
$options->getCacheDir(), |
1011
|
|
|
$this->getCacheId() |
1012
|
|
|
) |
1013
|
|
|
); |
1014
|
|
|
} |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
/* |
1018
|
|
|
* Step 3) Format the data - Translate - Replace - Date / time / datetime - Numbers - ... |
1019
|
|
|
*/ |
1020
|
|
|
$prepareData = new PrepareData($data, $this->getColumns()); |
1021
|
|
|
$prepareData->setRendererName($this->getRendererName()); |
1022
|
|
|
if ($this->hasTranslator()) { |
1023
|
|
|
$prepareData->setTranslator($this->getTranslator()); |
1024
|
|
|
} |
1025
|
|
|
$prepareData->prepare(); |
1026
|
|
|
$this->preparedData = $prepareData->getData(); |
1027
|
|
|
|
1028
|
|
|
$this->isDataLoaded = true; |
1029
|
|
|
} |
1030
|
|
|
|
1031
|
|
|
/** |
1032
|
|
|
* Render the grid |
1033
|
|
|
*/ |
1034
|
|
|
public function render() |
1035
|
|
|
{ |
1036
|
|
|
if ($this->isDataLoaded() === false) { |
1037
|
|
|
$this->loadData(); |
1038
|
|
|
} |
1039
|
|
|
|
1040
|
|
|
/** |
1041
|
|
|
* Step 4) Render the data to the defined output format (HTML, PDF...) |
1042
|
|
|
* - Styling the values based on column (and value) |
1043
|
|
|
*/ |
1044
|
|
|
$renderer = $this->getRenderer(); |
1045
|
|
|
$renderer->setTitle($this->getTitle()); |
1046
|
|
|
$renderer->setPaginator($this->getPaginator()); |
1047
|
|
|
$renderer->setData($this->getPreparedData()); |
1048
|
|
|
$renderer->prepareViewModel($this); |
1049
|
|
|
|
1050
|
|
|
$this->response = $renderer->execute(); |
1051
|
|
|
|
1052
|
|
|
$this->isRendered = true; |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
/** |
1056
|
|
|
* Is already rendered? |
1057
|
|
|
* |
1058
|
|
|
* @return boolean |
1059
|
|
|
*/ |
1060
|
|
|
public function isRendered() |
1061
|
|
|
{ |
1062
|
|
|
return (bool) $this->isRendered; |
1063
|
|
|
} |
1064
|
|
|
|
1065
|
|
|
/** |
1066
|
|
|
* |
1067
|
|
|
* @throws \Exception |
1068
|
|
|
* @return Paginator |
1069
|
|
|
*/ |
1070
|
|
|
public function getPaginator() |
1071
|
|
|
{ |
1072
|
|
|
if (null === $this->paginator) { |
1073
|
|
|
throw new \Exception('Paginator is only available after calling "loadData()"'); |
1074
|
|
|
} |
1075
|
|
|
|
1076
|
|
|
return $this->paginator; |
1077
|
|
|
} |
1078
|
|
|
|
1079
|
|
|
/** |
1080
|
|
|
* |
1081
|
|
|
* @return array |
1082
|
|
|
*/ |
1083
|
|
|
private function getPreparedData() |
1084
|
|
|
{ |
1085
|
|
|
return $this->preparedData; |
1086
|
|
|
} |
1087
|
|
|
|
1088
|
|
|
/** |
1089
|
|
|
* Set the toolbar view template |
1090
|
|
|
* |
1091
|
|
|
* @param string $name |
1092
|
|
|
*/ |
1093
|
|
|
public function setToolbarTemplate($name) |
1094
|
|
|
{ |
1095
|
|
|
$this->toolbarTemplate = (string) $name; |
1096
|
|
|
} |
1097
|
|
|
|
1098
|
|
|
/** |
1099
|
|
|
* Get the toolbar template name |
1100
|
|
|
* Return null if nothing custom set |
1101
|
|
|
* |
1102
|
|
|
* @return string|null |
1103
|
|
|
*/ |
1104
|
|
|
public function getToolbarTemplate() |
1105
|
|
|
{ |
1106
|
|
|
return $this->toolbarTemplate; |
1107
|
|
|
} |
1108
|
|
|
|
1109
|
|
|
/** |
1110
|
|
|
* Set the toolbar view template variables |
1111
|
|
|
* |
1112
|
|
|
* @param array $variables |
1113
|
|
|
*/ |
1114
|
|
|
public function setToolbarTemplateVariables(array $variables) |
1115
|
|
|
{ |
1116
|
|
|
$this->toolbarTemplateVariables = $variables; |
1117
|
|
|
} |
1118
|
|
|
|
1119
|
|
|
/** |
1120
|
|
|
* Get the toolbar template variables |
1121
|
|
|
* |
1122
|
|
|
* @return array |
1123
|
|
|
*/ |
1124
|
|
|
public function getToolbarTemplateVariables() |
1125
|
|
|
{ |
1126
|
|
|
return $this->toolbarTemplateVariables; |
1127
|
|
|
} |
1128
|
|
|
|
1129
|
|
|
/** |
1130
|
|
|
* Set a custom ViewModel...generally NOT necessary! |
1131
|
|
|
* |
1132
|
|
|
* @param ViewModel $viewModel |
1133
|
|
|
* @throws \Exception |
1134
|
|
|
*/ |
1135
|
|
|
public function setViewModel(ViewModel $viewModel) |
1136
|
|
|
{ |
1137
|
|
|
if ($this->viewModel !== null) { |
1138
|
|
|
throw new \Exception('A viewModel is already set. Did you already called $grid->render() or $grid->getViewModel() before?'); |
1139
|
|
|
} |
1140
|
|
|
|
1141
|
|
|
$this->viewModel = $viewModel; |
1142
|
|
|
} |
1143
|
|
|
|
1144
|
|
|
/** |
1145
|
|
|
* |
1146
|
|
|
* @return ViewModel |
1147
|
|
|
*/ |
1148
|
|
|
public function getViewModel() |
1149
|
|
|
{ |
1150
|
|
|
if (null === $this->viewModel) { |
1151
|
|
|
$this->viewModel = new ViewModel(); |
1152
|
|
|
} |
1153
|
|
|
|
1154
|
|
|
return $this->viewModel; |
1155
|
|
|
} |
1156
|
|
|
|
1157
|
|
|
/** |
1158
|
|
|
* |
1159
|
|
|
* @return \Zend\Stdlib\ResponseInterface|\Zend\Http\Response\Stream|\Zend\View\Model\ViewModel |
1160
|
|
|
*/ |
1161
|
|
|
public function getResponse() |
1162
|
|
|
{ |
1163
|
|
|
if (! $this->isRendered()) { |
1164
|
|
|
$this->render(); |
1165
|
|
|
} |
1166
|
|
|
|
1167
|
|
|
return $this->response; |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
|
|
/** |
1171
|
|
|
* Is this a HTML "init" response? |
1172
|
|
|
* YES: loading the HTML for the grid |
1173
|
|
|
* NO: AJAX loading OR it's an export |
1174
|
|
|
* |
1175
|
|
|
* @return boolean |
1176
|
|
|
*/ |
1177
|
|
|
public function isHtmlInitReponse() |
1178
|
|
|
{ |
1179
|
|
|
if (! $this->getResponse() instanceof JsonModel && ! $this->getResponse() instanceof ResponseInterface) { |
1180
|
|
|
return true; |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
|
|
return false; |
1184
|
|
|
} |
1185
|
|
|
|
1186
|
|
|
/** |
1187
|
|
|
* @param Renderer\AbstractRenderer $rendererService |
1188
|
|
|
* @return self |
1189
|
|
|
*/ |
1190
|
|
|
public function setRendererService(Renderer\AbstractRenderer $rendererService) |
1191
|
|
|
{ |
1192
|
|
|
$this->rendererService = $rendererService; |
1193
|
|
|
|
1194
|
|
|
return $this; |
1195
|
|
|
} |
1196
|
|
|
} |
1197
|
|
|
|
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.