Completed
Push — develop ( 59902e...99e88c )
by Bartko
01:49
created

Service::moveNode()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 24
nop 1
dl 0
loc 30
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * Quick end dirty solution only for demonstration purpose.
4
 */
5
declare(strict_types=1);
6
7
use StefanoTree\Exception\ValidationException;
8
use StefanoTree\TreeInterface;
9
10
session_start();
11
12
include_once __DIR__.'/../vendor/autoload.php';
13
14
$config = include_once __DIR__.'/config.php';
15
16
$dbAdapter = \Doctrine\DBAL\DriverManager::getConnection(
17
    $config['dbConnection'],
18
    new \Doctrine\DBAL\Configuration()
19
);
20
21
/**************************************
22
 *    Tree Adapter
23
 **************************************/
24
$treeAdapter = \StefanoTree\NestedSet::factory(
25
    array(
26
        'tableName' => 'categories',
27
        'idColumnName' => 'id',
28
        'sequenceName' => 'categories_id_seq',
29
        'scopeColumnName' => 'group_id',
30
    ),
31
    $dbAdapter
32
);
33
34
/***************************************
35
 * Join example
36
 ***************************************/
37
/*
38
$select = $dbAdapter->createQueryBuilder();
39
$select->from('categories' ,'c')
40
       ->select('c.*', '...')
41
       ->leftJoin('c', 'metadata', 'm', 'm.id = c.id');
42
43
$adapter = $treeAdapter
44
    ->getAdapter()
45
    ->setDefaultDbSelect($select);
46
*/
47
48
class Service
49
{
50
    private $treeAdapter;
51
52
    public function __construct(TreeInterface $treeAdapter)
53
    {
54
        $this->treeAdapter = $treeAdapter;
55
    }
56
57
    private function getTreeAdapter(): TreeInterface
58
    {
59
        return $this->treeAdapter;
60
    }
61
62
    public function createRoot(array $data): void
63
    {
64
        $errors = array();
65
66
        $label = $data['label'] ?? '';
67
        if (0 == strlen($label)) {
68
            $errors[] = 'Root Name cannot be empty.';
69
        } elseif (15 < strlen($label)) {
70
            $errors[] = 'Root Name is too long. Max length is 15 characters.';
71
        }
72
73
        $scope = $data['scope'] ?? '';
74
        if (0 === strlen($scope)) {
75
            $errors[] = 'Scope Name cannot be empty.';
76
        } elseif (!preg_match('|^[1-9][0-9]*$|', $scope)) {
77
            $errors[] = 'Scope Name must be integer.';
78
        } elseif (15 < strlen($scope)) {
79
            $errors[] = 'Scope Name is too long. Max length is 15 characters.';
80
        }
81
82
        if (count($errors)) {
83
            throw new ValidationError($errors);
84
        }
85
86
        $data = array(
87
            'name' => $label,
88
        );
89
90
        try {
91
            $this->getTreeAdapter()
92
                ->createRootNode($data, $scope);
93
        } catch (ValidationException $e) {
94
            throw new ValidationError([$e->getMessage()]);
95
        }
96
    }
97
98
    public function createNode(array $data): void
99
    {
100
        $errors = array();
101
102
        $targetId = $_POST['target_node_id'] ?? '';
103
        if (0 === strlen($targetId)) {
104
            $errors[] = 'Target Node cannot be empty.';
105
        }
106
107
        $label = $data['label'] ?? '';
108
        if (0 == strlen($label)) {
109
            $errors[] = 'Name cannot be empty.';
110
        } elseif (15 < strlen($label)) {
111
            $errors[] = 'Name is too long. Max length is 15 characters.';
112
        }
113
114
        $placement = $data['placement'] ?? '';
115
        if (0 == strlen($placement)) {
116
            $errors[] = 'Placement cannot be empty.';
117
        }
118
119
        $data = array(
120
            'name' => $label,
121
        );
122
123
        if (count($errors)) {
124
            throw new ValidationError($errors);
125
        }
126
127
        try {
128
            $this->getTreeAdapter()
129
                ->addNode($targetId, $data, $placement);
130
        } catch (ValidationException $e) {
131
            throw new ValidationError([$e->getMessage()]);
132
        }
133
    }
134
135
    public function deleteNode(array $data): void
136
    {
137
        $errors = array();
138
139
        $id = $data['id'] ?? '';
140
141
        if (0 == strlen($id)) {
142
            $errors[] = 'Id is missing. Cannot perform delete operation.';
143
        }
144
145
        if (count($errors)) {
146
            throw new ValidationError($errors);
147
        }
148
149
        $this->getTreeAdapter()
150
            ->deleteBranch($id);
151
    }
152
153
    public function updateNode(array $data): void
154
    {
155
        $errors = array();
156
157
        $nodeId = $_POST['node_id'] ?? '';
158
        if (0 === strlen($nodeId)) {
159
            $errors[] = 'Node cannot be empty.';
160
        }
161
162
        $label = $data['label'] ?? '';
163
        if (0 == strlen($label)) {
164
            $errors[] = 'Name cannot be empty.';
165
        } elseif (15 < strlen($label)) {
166
            $errors[] = 'Name is too long. Max length is 15 characters.';
167
        }
168
169
        $data = array(
170
            'name' => $label,
171
        );
172
173
        if (count($errors)) {
174
            throw new ValidationError($errors);
175
        }
176
177
        try {
178
            $this->getTreeAdapter()
179
                ->updateNode($nodeId, $data);
180
        } catch (ValidationException $e) {
181
            throw new ValidationError([$e->getMessage()]);
182
        }
183
    }
184
185
    public function moveNode(array $data): void
186
    {
187
        $errors = array();
188
189
        $sourceId = $data['source_node_id'] ?? '';
190
        if (0 == strlen($sourceId)) {
191
            $errors[] = 'Source Node cannot be empty.';
192
        }
193
194
        $targetId = $data['target_node_id'] ?? '';
195
        if (0 == strlen($targetId)) {
196
            $errors[] = 'Target Node cannot be empty.';
197
        }
198
199
        $placement = $data['placement'] ?? '';
200
        if (0 == strlen($placement)) {
201
            $errors[] = 'Placement cannot be empty.';
202
        }
203
204
        if (count($errors)) {
205
            throw new ValidationError($errors);
206
        }
207
208
        try {
209
            $this->getTreeAdapter()
210
                ->moveNode($sourceId, $targetId, $placement);
211
        } catch (ValidationException $e) {
212
            throw new ValidationError([$e->getMessage()]);
213
        }
214
    }
215
216
    public function getRoots(): array
217
    {
218
        return $this->getTreeAdapter()
219
                    ->getRoots();
220
    }
221
222
    public function getDescendants($nodeId): array
223
    {
224
        return $this->getTreeAdapter()
225
            ->getDescendantsQueryBuilder()
226
            ->get($nodeId);
227
    }
228
229
    public function findDescendants(array $criteria): array
230
    {
231
        $queryBuilder = $this->getTreeAdapter()
232
            ->getDescendantsQueryBuilder();
233
234
        $errors = array();
235
236
        $nodeId = $criteria['node_id'] ?? '';
237
        if (0 === strlen($nodeId)) {
238
            $errors[] = 'Node cannot be empty.';
239
        }
240
241
        $excludeFirstNLevel = $criteria['exclude_first_n_level'] ?? null;
242
        if (null !== $excludeFirstNLevel) {
243
            if (!preg_match('|^[0-9]*$|', $excludeFirstNLevel)) {
244
                $errors[] = 'Exclude First N Level  must be positive integer,';
245
            } else {
246
                $queryBuilder->excludeFirstNLevel((int) $excludeFirstNLevel);
247
            }
248
        }
249
250
        $levelLimit = $criteria['level_limit'] ?? null;
251
        if (null !== $levelLimit) {
252
            if (!preg_match('|^[0-9]*$|', $levelLimit)) {
253
                $errors[] = 'Level limit must be positive integer,';
254
            } else {
255
                $queryBuilder->levelLimit((int) $levelLimit);
256
            }
257
        }
258
259
        $excludeBranch = $criteria['exclude_node_id'] ?? null;
260
        if (null !== $excludeBranch) {
261
            $queryBuilder->excludeBranch($excludeBranch);
262
        }
263
264
        if (count($errors)) {
265
            throw new ValidationError($errors);
266
        }
267
268
        return $queryBuilder->get($nodeId);
269
    }
270
271
    public function findAncestors(array $criteria): array
272
    {
273
        $queryBuilder = $this->getTreeAdapter()
274
            ->getAncestorsQueryBuilder();
275
276
        $errors = array();
277
278
        $nodeId = $criteria['node_id'] ?? '';
279
        if (0 === strlen($nodeId)) {
280
            $errors[] = 'Node cannot be empty.';
281
        }
282
283
        $excludeFirstNLevel = $criteria['exclude_first_n_level'] ?? null;
284
        if (null !== $excludeFirstNLevel) {
285
            if (!preg_match('|^[0-9]*$|', $excludeFirstNLevel)) {
286
                $errors[] = 'Exclude First N Level  must be positive integer,';
287
            } else {
288
                $queryBuilder->excludeFirstNLevel((int) $excludeFirstNLevel);
289
            }
290
        }
291
292
        $excludeLastNLevel = $criteria['exclude_last_n_level'] ?? null;
293
        if (null !== $excludeLastNLevel) {
294
            if (!preg_match('|^[0-9]*$|', $excludeLastNLevel)) {
295
                $errors[] = 'Exclude Last N Level  must be positive integer,';
296
            } else {
297
                $queryBuilder->excludeLastNLevel((int) $excludeLastNLevel);
298
            }
299
        }
300
301
        if (count($errors)) {
302
            throw new ValidationError($errors);
303
        }
304
305
        return $queryBuilder->get($nodeId);
306
    }
307
}
308
309
class ViewHelper
310
{
311
    public function escape($string): string
312
    {
313
        return htmlspecialchars((string) $string);
314
    }
315
316
    public function renderTree(array $nodes): string
317
    {
318
        $html = '';
319
320
        $previousLevel = -1;
321
        foreach ($nodes as $node) {
322
            if ($previousLevel > $node['level']) {
323
                for ($i = $node['level']; $previousLevel > $i; ++$i) {
324
                    $html = $html.'</li></ul>';
325
                }
326
                $html = $html.'</li>';
327
            } elseif ($previousLevel < $node['level']) {
328
                $html = $html.'<ul>';
329
            } else {
330
                $html = $html.'</li>';
331
            }
332
333
            $html = $html.'<li>';
334
            $html = $html.'<span>'.$this->escape($node['name']).'</span>'
335
                .' <a href="/?action=delete&id='.$this->escape($node['id']).'" class="badge badge-danger">Delete</a>';
336
337
            $previousLevel = $node['level'];
338
        }
339
340
        for ($i = -1; $previousLevel > $i; ++$i) {
341
            $html = $html.'</li></ul>';
342
        }
343
344
        return $html;
345
    }
346
347
    public function renderBreadcrumbs(array $nodes): string
348
    {
349
        $html = '';
350
351
        foreach ($nodes as $node) {
352
            $html .= '<a class="breadcrumb-item" href="#">'.$this->escape($node['name']).'</a>';
353
        }
354
355
        return '<nav class="breadcrumb">'.$html.'</nav>';
356
    }
357
358
    public function renderSelectOptions(array $nodes): string
359
    {
360
        $pathCache = array();
361
362
        $html = '';
363
364
        foreach ($nodes as $node) {
365
            if (!$node['parent_id']) {
366
                $pathCache[$node['id']] = '/'.$node['name'];
367
            } else {
368
                $pathCache[$node['id']] = $pathCache[$node['parent_id']].'/'.$node['name'];
369
            }
370
            $html .= '<option value="'.$this->escape($node['id']).'">'.$this->escape($pathCache[$node['id']]).'</option>';
371
        }
372
373
        return '<option value="">---</option>'.$html;
374
    }
375
376
    public function renderPlacementOptions(): string
377
    {
378
        $placements = array(
379
            TreeInterface::PLACEMENT_CHILD_TOP => 'Child Top',
380
            TreeInterface::PLACEMENT_CHILD_BOTTOM => 'Child Bottom',
381
            TreeInterface::PLACEMENT_TOP => 'Top',
382
            TreeInterface::PLACEMENT_BOTTOM => 'Bottom',
383
        );
384
385
        $html = '';
386
387
        foreach ($placements as $id => $placement) {
388
            $html .= '<option value="'.$this->escape($id).'">'.$this->escape($placement).'</option>';
389
        }
390
391
        return '<option value="">---</option>'.$html;
392
    }
393
394
    public function renderErrorMessages(array $errors): string
395
    {
396
        $html = '';
397
398
        foreach ($errors as $error) {
399
            $html .= '<li>'.$this->escape($error).'</li>';
400
        }
401
402
        return '<div class="alert alert-danger"><ul class="error-container">'.$html.'</ul></div>';
403
    }
404
405
    public function renderSuccessMessage(string $message): string
406
    {
407
        return '<div class="alert alert-success">'.$this->escape($message).'</div>';
408
    }
409
410
    public function renderFlashMessage(): string
411
    {
412
        $message = $_SESSION['flashMessage'] ?? '';
413
414
        if ($message) {
415
            unset($_SESSION['flashMessage']);
416
417
            return $this->renderSuccessMessage($message);
418
        } else {
419
            return '';
420
        }
421
    }
422
}
423
424
class ValidationError extends \Exception
425
{
426
    private $errorMessages = array();
427
428
    public function __construct(array $errorMessages = array())
429
    {
430
        $this->errorMessages = $errorMessages;
431
    }
432
433
    public function addError(string $error): void
434
    {
435
        $this->errorMessages[] = $error;
436
    }
437
438
    public function addErrors(array $errors): void
439
    {
440
        $this->errorMessages = array_merge(array_values($errors), $this->errorMessages);
441
    }
442
443
    public function getErrors(): array
444
    {
445
        return $this->errorMessages;
446
    }
447
}
448
449
function setFlashMessageAndRedirect(string $message, string $url)
450
{
451
    $_SESSION['flashMessage'] = $message;
452
    $redirectUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http')."://$_SERVER[HTTP_HOST]$url";
453
    header(sprintf('Location: %s', $redirectUrl));
454
    die();
455
}
456
457
/************************************
458
 *    Router
459
 ***********************************/
460
$service = new Service($treeAdapter);
461
462
try {
463
    switch ($_GET['action'] ?? '') {
464
        case 'create-scope':
465
            $service->createRoot($_POST);
466
            setFlashMessageAndRedirect('New root node and scope was successfully created.', '/');
467
            break;
468
        case 'create-node':
469
            $service->createNode($_POST);
470
            setFlashMessageAndRedirect('New node was successfully created.', '/');
471
            break;
472
        case 'move-node':
473
            $service->moveNode($_POST);
474
            setFlashMessageAndRedirect('Branch/Node was successfully moved.', '/');
475
            break;
476
        case 'update-node':
477
            $service->updateNode($_POST);
478
            setFlashMessageAndRedirect('Node was successfully updated.', '/');
479
            break;
480
        case 'delete':
481
            $service->deleteNode($_GET);
482
            setFlashMessageAndRedirect('Branch/Node was successfully deleted.', '/');
483
            break;
484
        case 'descendant-test':
485
            $descendants = $service->findDescendants($_GET);
486
            $showDescendantTestBlock = true;
487
            break;
488
        case 'ancestor-test':
489
            $ancestors = $service->findAncestors($_GET);
490
            $showAncestorTestBlock = true;
491
            break;
492
    }
493
} catch (ValidationError $e) {
494
    $errorMessage = $e->getErrors();
495
}
496
497
/************************************
498
 *   View
499
 ***********************************/
500
$wh = new ViewHelper();
501
?>
502
503
<html>
504
    <head>
505
        <title>Demo</title>
506
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
507
        <style>
508
            .error-container {
509
                margin-bottom: 0;
510
                padding-left: 1rem;
511
            }
512
        </style>
513
    </head>
514
    <body>
515
        <div class="container-fluid">
516
            <h1><a href="/">Demo</a></h1>
517
518
            <?php
519
            if ($errorMessage ?? false) {
520
                echo $wh->renderErrorMessages($errorMessage);
521
            }
522
523
            echo $wh->renderFlashMessage();
524
            ?>
525
526
            <div class="row">
527
                <div class="col-sm-4">
528
                    <form action="/?action=create-scope" method="post">
529
                        <div class="form-group">
530
                            <label>Root name</label>
531
                            <input type="text" name="label" class="form-control" />
532
                        </div>
533
                        <div class="form-group">
534
                            <label>Scope name</label>
535
                            <input type="text" name="scope" class="form-control" />
536
                        </div>
537
                        <input type="submit" value="Create" class="btn btn-primary" />
538
                    </form>
539
                </div>
540
            </div>
541
            <?php
542
            foreach ($service->getRoots() as $root) {
543
                $nodes = $service->getDescendants($root['id']); ?>
544
                <hr />
545
                <h2>Scope - <?php echo $wh->escape($root['group_id']); ?></h2>
546
547
                <div class="row">
548
                    <div class="col-sm-2">
549
                        <h3>Create</h3>
550
                        <form action="/?action=create-node" method="post">
551
                            <div class="form-group">
552
                                <label>Name</label>
553
                                <input type="text" name="label" class="form-control" />
554
                            </div>
555
                            <div class="form-group">
556
                                <label>Target Node</label>
557
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
558
                            </div>
559
                            <div class="form-group">
560
                                <label>Placement</label>
561
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
562
                            </div>
563
                            <div class="form-group">
564
                                <input type="submit" value="Create" class="btn btn-primary" />
565
                            </div>
566
                        </form>
567
                    </div>
568
569
                    <div class="col-sm-2">
570
                        <h3>Move</h3>
571
                        <form action="/?action=move-node" method="post">
572
                            <div class="form-group">
573
                                <label>Source Node</label>
574
                                <select name="source_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
575
                            </div>
576
                            <div class="form-group">
577
                                <label>Target Node</label>
578
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
579
                            </div>
580
                            <div class="form-group">
581
                                <label>Placement</label>
582
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
583
                            </div>
584
                            <input type="submit" value="Move" class="btn btn-primary" />
585
                        </form>
586
                    </div>
587
588
                    <div class="col-sm-2">
589
                        <h3>Update</h3>
590
                        <form action="/?action=update-node" method="post">
591
                            <div class="form-group">
592
                                <label>Name</label>
593
                                <input type="text" name="label" class="form-control" />
594
                            </div>
595
                            <div class="form-group">
596
                                <label>Node</label>
597
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
598
                            </div>
599
                            <input type="submit" value="Update" class="btn btn-primary" />
600
                        </form>
601
                    </div>
602
603
                    <div class="col-sm-3">
604
                        <h3>Descendant Test</h3>
605
                        <form action="/" method="get">
606
                            <div class="form-group">
607
                                <label>Node</label>
608
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
609
                            </div>
610
                            <div class="form-group">
611
                                <label>Exclude First N Level</label>
612
                                <input type="number" min="0" step="1" name="exclude_first_n_level" class="form-control" />
613
                            </div>
614
                            <div class="form-group">
615
                                <label>Level Limit</label>
616
                                <input type="number" min="0" step="1" name="level_limit" class="form-control" />
617
                            </div>
618
                            <div class="form-group">
619
                                <label>Exclude Branch</label>
620
                                <select name="exclude_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
621
                            </div>
622
                            <input type="hidden" name="action" value="descendant-test" />
623
                            <input type="hidden" name="scope" value="<?php echo $wh->escape($root['group_id']); ?>" />
624
                            <input type="submit" value="Show" class="btn btn-primary" />
625
                        </form>
626
                    </div>
627
628
                    <div class="col-sm-3">
629
                        <h3>Ancestor Test</h3>
630
                        <form action="/" method="get">
631
                            <div class="form-group">
632
                                <label>Node</label>
633
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
634
                            </div>
635
                            <div class="form-group">
636
                                <label>Exclude First N Level</label>
637
                                <input type="number" min="0" step="1" name="exclude_first_n_level" class="form-control" />
638
                            </div>
639
                            <div class="form-group">
640
                                <label>Exclude Last N Level</label>
641
                                <input type="number" min="0" step="1" name="exclude_last_n_level" class="form-control" />
642
                            </div>
643
                            <input type="hidden" name="action" value="ancestor-test" />
644
                            <input type="hidden" name="scope" value="<?php echo $wh->escape($root['group_id']); ?>" />
645
                            <input type="submit" value="Show" class="btn btn-primary" />
646
                        </form>
647
                    </div>
648
                </div>
649
650
                <hr />
651
652
                <div class="row">
653
                    <div class="col-sm-6">
654
                        <h3>Whole Tree</h3>
655
                        <?php echo $wh->renderTree($nodes); ?>
656
                    </div>
657
                    <div class="col-sm-6">
658
                        <?php
659
                        if (($showDescendantTestBlock ?? false) && $root['group_id'] == $_GET['scope']) {
660
                            ?>
661
                            <h3>Descendants Test Result</h3>
662
                            <?php
663
                            if (0 == count($descendants)) {
664
                                echo $wh->renderErrorMessages(['No descendants was found']);
665
                            } else {
666
                                echo $wh->renderTree($descendants);
667
                            } ?>
668
                        <?php
669
                        } ?>
670
671
                        <?php
672
                        if (($showAncestorTestBlock ?? false) && $root['group_id'] == $_GET['scope']) {
673
                            ?>
674
                            <h3>Ancestors Test Result</h3>
675
                            <?php
676
                            if (0 == count($ancestors)) {
677
                                echo $wh->renderErrorMessages(['No ancestors was found']);
678
                            } else {
679
                                echo $wh->renderBreadcrumbs($ancestors);
680
                                echo $wh->renderTree($ancestors);
681
                            } ?>
682
                            <?php
683
                        } ?>
684
                    </div>
685
                </div>
686
            <?php
687
            }?>
688
        </div>
689
    </body>
690
</html>