Completed
Push — wip-platform ( 4ae41a...a65a23 )
by
unknown
02:30
created

CoreAdmin::getCurrentComposition()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 10
nc 8
nop 0
1
<?php
2
3
/*
4
 *
5
 * Copyright (C) 2015-2017 Libre Informatique
6
 *
7
 * This file is licenced under the GNU LGPL v3.
8
 * For the full copyright and license information, please view the LICENSE.md
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Blast\Bundle\CoreBundle\Admin;
13
14
use Sonata\AdminBundle\Datagrid\DatagridMapper;
15
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
16
use Sonata\AdminBundle\Datagrid\ListMapper;
17
use Sonata\AdminBundle\Form\FormMapper;
18
use Sonata\AdminBundle\Mapper\BaseMapper;
19
use Sonata\AdminBundle\Show\ShowMapper;
20
use Sonata\AdminBundle\Route\RouteCollection;
21
use Sonata\AdminBundle\Admin\AbstractAdmin as SonataAdmin;
22
use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
23
use Blast\Bundle\CoreBundle\Tools\Reflection\ClassAnalyzer;
24
use Blast\Bundle\CoreBundle\Admin\Traits\CollectionsManager;
25
use Blast\Bundle\CoreBundle\Admin\Traits\Mapper;
26
use Blast\Bundle\CoreBundle\Admin\Traits\Templates;
27
use Blast\Bundle\CoreBundle\Admin\Traits\PreEvents;
28
use Blast\Bundle\CoreBundle\Admin\Traits\ManyToManyManager;
29
use Blast\Bundle\CoreBundle\Admin\Traits\Actions;
30
use Blast\Bundle\CoreBundle\Admin\Traits\ListActions;
31
use Blast\Bundle\CoreBundle\CodeGenerator\CodeGeneratorRegistry;
32
use Blast\Bundle\CoreBundle\Translator\LibrinfoLabelTranslatorStrategy;
33
use Symfony\Component\PropertyAccess\PropertyAccess;
34
35
abstract class CoreAdmin extends SonataAdmin implements \JsonSerializable
36
{
37
    use CollectionsManager,
38
        ManyToManyManager,
39
        Mapper,
40
        Templates,
41
        PreEvents,
42
        Actions,
43
        ListActions
44
    ;
45
46
    protected $extraTemplates = [];
47
48
    public function configure()
49
    {
50
        parent::configure();
51
52
        /* Default Translation Strategy if not set as admin service tags */
53
        /* @todo : find if it is a good idea or not */
54
        if (!($this->getLabelTranslatorStrategy() instanceof LibrinfoLabelTranslatorStrategy)) {
55
            $this->setLabelTranslatorStrategy(new LibrinfoLabelTranslatorStrategy());
56
        }
57
        /* Should always be */
58
        if ($this->getLabelTranslatorStrategy() instanceof LibrinfoLabelTranslatorStrategy) {
59
            $this->getLabelTranslatorStrategy()->setFix($this->getClassnameLabel());
60
        }
61
62
        /* @todo: apply TranslatorStrategy to form_tab and form_group and show_tab and
63
           ... warning it may impact code as it used in some postConfigureFormFields */
64
    }
65
66
    /**
67
     * Configure routes for list actions.
68
     *
69
     * @param RouteCollection $collection
70
     */
71
    protected function configureRoutes(RouteCollection $collection)
72
    {
73
        parent::configureRoutes($collection);
74
        $collection->add('duplicate', $this->getRouterIdParameter() . '/duplicate');
75
        $collection->add('generateEntityCode');
76
77
        /* Needed or not needed ...
78
         * in sonata-project/admin-bundle/Controller/CRUDController.php
79
         * the batchAction method
80
         * throw exception if the http method is not POST
81
        */
82
        if ($collection->get('batch')) {
83
            $collection->get('batch')->setMethods(['POST']);
84
        }
85
    }
86
87 View Code Duplication
    public function getBaseRouteName()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
88
    {
89
        $configuredBaseRoute = $this->getBaseRouteMapping();
90
91
        if (count($configuredBaseRoute) > 0) {
92
            $this->cachedBaseRouteName = null;
0 ignored issues
show
Bug introduced by
The property cachedBaseRouteName cannot be accessed from this context as it is declared private in class Sonata\AdminBundle\Admin\AbstractAdmin.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
93
            if (isset($configuredBaseRoute['name']) && $this->baseRouteName === null) {
94
                $this->baseRouteName = $configuredBaseRoute['name'];
95
            }
96
        }
97
98
        return parent::getBaseRouteName();
99
    }
100
101 View Code Duplication
    public function getBaseRoutePattern()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
102
    {
103
        $configuredBaseRoute = $this->getBaseRouteMapping();
104
105
        if (count($configuredBaseRoute) > 0) {
106
            $this->cachedBaseRoutePattern = null;
0 ignored issues
show
Bug introduced by
The property cachedBaseRoutePattern cannot be accessed from this context as it is declared private in class Sonata\AdminBundle\Admin\AbstractAdmin.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
107
            if (isset($configuredBaseRoute['pattern']) && $this->baseRoutePattern === null) {
108
                $this->baseRoutePattern = $configuredBaseRoute['pattern'];
109
            }
110
        }
111
112
        return parent::getBaseRoutePattern();
113
    }
114
115
    public function getFormTheme()
116
    {
117
        return array_merge($this->formTheme, $this->getFormThemeMapping());
118
    }
119
120
    /**
121
     * @param DatagridMapper $mapper
122
     */
123
    protected function configureDatagridFilters(DatagridMapper $mapper)
124
    {
125
        if (!$this->configureMapper($mapper)) {
126
            $this->fallbackConfiguration($mapper, __FUNCTION__);
127
        }
128
    }
129
130
    /**
131
     * @param ListMapper $mapper
132
     */
133
    protected function configureListFields(ListMapper $mapper)
134
    {
135
        if (!$this->configureMapper($mapper)) {
136
            $this->fallbackConfiguration($mapper, __FUNCTION__);
137
        }
138
    }
139
140
    /**
141
     * @param FormMapper $mapper
142
     */
143
    protected function configureFormFields(FormMapper $mapper)
144
    {
145
        if (!$this->configureMapper($mapper)) {
146
            $this->fallbackConfiguration($mapper, __FUNCTION__);
147
        }
148
    }
149
150
    /**
151
     * @param ShowMapper $mapper
152
     */
153
    protected function configureShowFields(ShowMapper $mapper)
154
    {
155
        if (!$this->configureMapper($mapper)) {
156
            $this->fallbackConfiguration($mapper, __FUNCTION__);
157
        }
158
    }
159
160
    /**
161
     * @param BaseMapper $mapper
162
     */
163
    protected function fixShowRoutes(BaseMapper $mapper)
164
    {
165
        foreach (['getShow', 'getList'] as $fct) {
166
            foreach ($this->$fct()->getElements() as $field) {
167
                if ($field instanceof FieldDescription) {
168
                    $options = $field->getOptions();
169
                    if ($options['route']['name'] != 'edit') {
170
                        continue;
171
                    }
172
173
                    $options['route']['name'] = 'show';
174
                    $field->setOptions($options);
175
                }
176
            }
177
        }
178
179
        return $this;
180
    }
181
182
    protected function getCurrentComposition()
183
    {
184
        // traits of the current Entity
185
        $classes = ClassAnalyzer::getTraits($this->getClass());
186
187
        // inheritance of the current Entity
188
        foreach (array_reverse([$this->getClass()] + class_parents($this->getClass())) as $class) {
189
            $classes[] = $class;
190
        }
191
192
        $interfaces = ClassAnalyzer::getInterfaces($this->getClass());
193
194
        // implementations of the current Entity
195
        foreach (array_reverse($interfaces) as $interface) {
196
            $classes[] = $interface;
197
        }
198
199
        // inheritance of the current Admin
200
        foreach (array_reverse([$this->getOriginalClass()] + $this->getParentClasses()) as $admin) {
201
            $classes[] = $admin;
202
        }
203
204
        return $classes;
205
    }
206
207
    private function fallbackConfiguration(BaseMapper $mapper, $function)
208
    {
209
        // fallback
210
        $rm = new \ReflectionMethod($this->getParentClass(), $function);
211
        if ($rm->class == $this->getParentClass()) {
212
            $this->configureFields($function, $mapper, $this->getParentClass());
213
        }
214
    }
215
216
    /**
217
     * Returns the level of depth of an array.
218
     *
219
     * @param array $array
220
     * @param int   $level : do not use, just used for recursivity
221
     *
222
     * @return int : depth
223
     */
224
    private static function arrayDepth($array, $level = 0)
225
    {
226
        if (!$array) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
227
            return $level;
228
        }
229
230
        if (!is_array($array)) {
231
            return $level;
232
        }
233
234
        ++$level;
235
        foreach ($array as $key => $value) {
236
            if (is_array($value)) {
237
                $level = $level < self::arrayDepth($value, $level) ? self::arrayDepth($value, $level) : $level;
238
            }
239
        }
240
241
        return $level;
242
    }
243
244
    protected function getOriginalClass()
245
    {
246
        return get_called_class();
247
    }
248
249
    protected function getParentClasses()
250
    {
251
        return class_parents($this->getOriginalClass());
252
    }
253
254
    protected function getParentClass()
255
    {
256
        return get_parent_class($this->getOriginalClass());
257
    }
258
259
    protected function getGrandParentClass()
260
    {
261
        return get_parent_class(get_parent_class($this->getOriginalClass()));
262
    }
263
264
    /**
265
     * @param string $view     'list', 'show', 'form', etc
266
     * @param string $template template name
267
     */
268
    public function addExtraTemplate($view, $template)
269
    {
270
        if (empty($this->extraTemplates[$view])) {
271
            $this->extraTemplates[$view] = [];
272
        }
273
        if (!in_array($template, $this->extraTemplates[$view])) {
274
            $this->extraTemplates[$view][] = $template;
275
        }
276
    }
277
278
    /**
279
     * @param string $view 'list', 'show', 'form', etc
280
     *
281
     * @return array array of template names
282
     */
283
    public function getExtraTemplates($view)
284
    {
285
        if (empty($this->extraTemplates[$view])) {
286
            $this->extraTemplates[$view] = [];
287
        }
288
289
        return $this->extraTemplates[$view];
290
    }
291
292
    /**
293
     * @param string $view 'list', 'show', 'form', etc
294
     * @param array  $link link (array keys should be: 'label', 'url', 'class', 'title')
295
     */
296
    public function addHelperLink($view, $link)
297
    {
298
        if (empty($this->helperLinks[$view])) {
299
            $this->helperLinks[$view] = [];
300
        }
301
302
        // Do not add links without URL
303
        if (empty($link['url'])) {
304
            return;
305
        }
306
307
        // Do not add two links with the same URL
308
        foreach ($this->helperLinks[$view] as $l) {
309
            if ($l['url'] == $link['url']) {
310
                return;
311
            }
312
        }
313
314
        $this->helperLinks[$view][] = $link;
315
    }
316
317
    /**
318
     * @param string $view 'list', 'show', 'form', etc
319
     *
320
     * @return array array of links (each link is an array with keys 'label', 'url', 'class' and 'title')
321
     */
322
    public function getHelperLinks($view)
323
    {
324
        if (empty($this->helperLinks[$view])) {
325
            $this->helperLinks[$view] = [];
326
        }
327
328
        return $this->helperLinks[$view];
329
    }
330
331
    /**
332
     * Checks if a Bundle is installed.
333
     *
334
     * @param string $bundle Bundle name or class FQN
335
     */
336
    public function bundleExists($bundle)
337
    {
338
        $kernelBundles = $this->getConfigurationPool()->getContainer()->getParameter('kernel.bundles');
339
        if (array_key_exists($bundle, $kernelBundles)) {
340
            return true;
341
        }
342
        if (in_array($bundle, $kernelBundles)) {
343
            return true;
344
        }
345
346
        return false;
347
    }
348
349
    /**
350
     * Rename a form tab after form fields have been configured.
351
     *
352
     * TODO: groups of the renamed tab are still prefixed with the old tab name
353
     *
354
     * @param type $tabName    the name of the tab to be renamed
355
     * @param type $newTabName the new name for the tab
356
     */
357 View Code Duplication
    public function renameFormTab($tabName, $newTabName, $keepOrder = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
358
    {
359
        $tabs = $this->getFormTabs();
360
361
        if (!$tabs) {
362
            return;
363
        }
364
365
        if (!isset($tabs[$tabName])) {
366
            throw new \Exception(sprintf('Tab %s does not exist.', $tabName));
367
        }
368
        if (isset($tabs[$newTabName])) {
369
            return;
370
        }
371
372
        if ($keepOrder) {
373
            $keys = array_keys($tabs);
374
            $keys[array_search($tabName, $keys)] = $newTabName;
375
            $tabs = array_combine($keys, $tabs);
376
        } else {
377
            $tabs[$newTabName] = $tabs[$tabName];
378
            unset($tabs[$tabName]);
379
        }
380
381
        $this->setFormTabs($tabs);
382
    }
383
384
    /**
385
     * Rename a show tab after show fields have been configured.
386
     *
387
     * TODO: groups of the renamed tab are still prefixed with the old tab name
388
     *
389
     * @param type $tabName    the name of the tab to be renamed
390
     * @param type $newTabName the new name for the tab
391
     */
392 View Code Duplication
    public function renameShowTab($tabName, $newTabName, $keepOrder = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
393
    {
394
        $tabs = $this->getShowTabs();
395
396
        if (!$tabs) {
397
            return;
398
        }
399
400
        if (!isset($tabs[$tabName])) {
401
            throw new \Exception(sprintf('Tab %s does not exist.', $tabName));
402
        }
403
        if (isset($tabs[$newTabName])) {
404
            return;
405
        }
406
407
        if ($keepOrder) {
408
            $keys = array_keys($tabs);
409
            $keys[array_search($tabName, $keys)] = $newTabName;
410
            $tabs = array_combine($keys, $tabs);
411
        } else {
412
            $tabs[$newTabName] = $tabs[$tabName];
413
            unset($tabs[$tabName]);
414
        }
415
416
        $this->setShowTabs($tabs);
417
    }
418
419
    /**
420
     * Rename a form group.
421
     *
422
     * @param string $group        the old group name
423
     * @param string $tab          the tab the group belongs to
424
     * @param string $newGroupName the new group name
425
     *
426
     * @return self
427
     */
428
    public function renameFormGroup($group, $tab, $newGroupName)
429
    {
430
        $groups = $this->getFormGroups();
431
432
        // When the default tab is used, the tabname is not prepended to the index in the group array
433
        if ($tab !== 'default') {
434
            $group = $tab . '.' . $group;
435
        }
436
        $newGroup = ($tab !== 'default') ? $tab . '.' . $newGroupName : $newGroupName;
437
438
        if (isset($groups[$newGroup])) {
439
            throw new \Exception(sprintf('%s form group already exists.', $newGroup));
440
        }
441
        if (!array_key_exists($group, $groups)) {
442
            throw new \Exception(sprintf('form group « %s » doesn\'t exist.', $group));
443
        }
444
445
        $groups[$newGroup] = $groups[$group];
446
        $groups[$newGroup]['name'] = $newGroupName;
447
        unset($groups[$group]);
448
449
        $tabs = $this->getFormTabs();
450
        $key = array_search($group, $tabs[$tab]['groups']);
451
452
        if (false !== $key) {
453
            $tabs[$tab]['groups'][$key] = $newGroup;
454
        }
455
456
        $this->setFormTabs($tabs);
457
        $this->setFormGroups($groups);
458
459
        return $this;
460
    }
461
462
    /**
463
     * Removes tab in current form Mapper.
464
     *
465
     * @param string|array $tabNames name or array of names of tabs to be removed
466
     * @param FormMapper   $mapper   Sonata Admin form mapper
467
     */
468
    public function removeTab($tabNames, $mapper)
469
    {
470
        $currentTabs = $this->getFormTabs();
471
        foreach ($currentTabs as $k => $item) {
0 ignored issues
show
Bug introduced by
The expression $currentTabs of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
472
            if (is_array($tabNames) && in_array($item['name'], $tabNames) || !is_array($tabNames) && $item['name'] === $tabNames) {
473
                foreach ($item['groups'] as $groupName) {
474
                    $this->removeAllFieldsFromFormGroup($groupName, $mapper);
475
                }
476
                unset($currentTabs[$k]);
477
            }
478
        }
479
        $this->setFormTabs($currentTabs);
480
    }
481
482
    /**
483
     * Removes all fields from form groups and remove them from mapper.
484
     *
485
     * @param string     $groupName Name of the group to remove
486
     * @param FormMapper $mapper    Sonata Admin form mapper
487
     */
488
    public function removeAllFieldsFromFormGroup($groupName, $mapper)
489
    {
490
        $formGroups = $this->getFormGroups();
491
        foreach ($formGroups as $name => $formGroup) {
0 ignored issues
show
Bug introduced by
The expression $formGroups of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
492
            if ($name === $groupName) {
493
                foreach ($formGroups[$name]['fields'] as $key => $field) {
494
                    $mapper->remove($key);
495
                }
496
            }
497
        }
498
    }
499
500
    public function jsonSerialize()
501
    {
502
        $propertiesToShow = [
503
            'baseRouteName',
504
            'baseRoutePattern',
505
            'extraTemplates',
506
            'listFieldDescriptions',
507
            'showFieldDescriptions',
508
            'formFieldDescriptions',
509
            'filterFieldDescriptions',
510
            'maxPerPage',
511
            'maxPageLinks',
512
            'classnameLabel',
513
            'translationDomain',
514
            'formOptions',
515
            'datagridValues',
516
            'perPageOptions',
517
            'pagerType',
518
            'code',
519
            'label',
520
            'routes',
521
            'subject',
522
            'children',
523
            'parent',
524
            'baseCodeRoute',
525
            'uniqid',
526
            'extensions',
527
            'class',
528
            'subClasses',
529
            'list',
530
            'show',
531
            'form',
532
            'filter',
533
            'formGroups',
534
            'formTabs',
535
            'showGroups',
536
            'showTabs',
537
            'managedCollections',
538
            'helperLinks',
539
            'titles',
540
        ];
541
542
        $properties = [];
543
        foreach ($this as $key => $value) {
544
            if (in_array($key, $propertiesToShow)) {
545
                $properties[$key] = $value;
546
            }
547
        }
548
549
        return $properties;
550
    }
551
552
    /**
553
     * {@inheritdoc}
554
     */
555
    public function prePersist($object)
556
    {
557
        parent::prePersist($object);
558
559
        $hasCodeGenerator = CodeGeneratorRegistry::hasGeneratorForClass(get_class($object));
560
        if ($hasCodeGenerator) {
561
            $accessor = PropertyAccess::createPropertyAccessor();
562
            foreach (CodeGeneratorRegistry::getCodeGenerators(get_class($object)) as $name => $generator) {
563
                $accessor->setValue($object, $name, $generator->generate($object));
564
            }
565
        }
566
    }
567
568
    public function getFlashManager()
569
    {
570
        return $this->getConfigurationPool()->getContainer()->get('sonata.core.flashmessage.manager');
571
    }
572
573
    /**
574
     * {@inheritdoc}
575
     */
576
    public function preBatchAction($actionName, ProxyQueryInterface $query, array &$idx, $allElements)
577
    {
578
        parent::preBatchAction($actionName, $query, $idx, $allElements);
579
580
        if ($actionName === 'delete') {
581
            $cascadingRelationChecker = $this->getConfigurationPool()->getContainer()->get('blast_core.doctrine.orm.cascading_relation_checker');
582
583
            foreach ($idx as $id) {
584
                $entity = $this->getModelManager()->find($this->getClass(), $id);
585
586
                if ($entity !== null) {
587
                    $undeletableAssociations = $cascadingRelationChecker->beforeEntityDelete($entity, $idx);
588
589
                    if (count($undeletableAssociations) > 0) {
590
                        foreach ($undeletableAssociations as $key => $undeletableAssociation) {
591
                            $undeletableAssociations[$key] = $this->getConfigurationPool()->getContainer()->get('translator')->trans('blast.doctrine_relations.' . $undeletableAssociation, [], 'messages');
592
                        }
593
594
                        $errorMessage = 'Cannot delete "%entity%" because it has remaining relation(s) %relations%';
595
596
                        $message = $this->getTranslator()->trans(
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...tAdmin::getTranslator() has been deprecated with message: since 3.9, to be removed with 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
597
                            $errorMessage,
598
                            [
599
                                '%relations%' => trim(implode(', ', $undeletableAssociations)),
600
                                '%entity%'    => (string) $entity,
601
                            ],
602
                            'SonataCoreBundle'
603
                        );
604
605
                        $this->getConfigurationPool()->getContainer()->get('session')->getFlashBag()->add('warning', $message);
606
                    }
607
                }
608
            }
609
        }
610
    }
611
}
612