Completed
Push — develop ( c08e7b...416a58 )
by Bartko
05:25
created

ValidationError   A

Complexity

Total Complexity 4

Size/Duplication

Total Lines 24
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 10
c 0
b 0
f 0
wmc 4
lcom 1
cbo 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A addError() 0 4 1
A addErrors() 0 4 1
A getErrors() 0 4 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 34 and the first side effect is on line 10.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
    new \StefanoTree\NestedSet\Options(array(
26
        'tableName' => 'categories',
27
        'idColumnName' => 'id',
28
        'sequenceName' => 'categories_id_seq',
29
        'scopeColumnName' => 'group_id',
30
    )),
31
    $dbAdapter
32
);
33
34
class Service
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
35
{
36
    private $treeAdapter;
37
38
    public function __construct(TreeInterface $treeAdapter)
39
    {
40
        $this->treeAdapter = $treeAdapter;
41
    }
42
43
    private function getTreeAdapter(): TreeInterface
44
    {
45
        return $this->treeAdapter;
46
    }
47
48
    public function createRoot(array $data): void
49
    {
50
        $errors = array();
51
52
        $label = $data['label'] ?? '';
53
        if (0 == strlen($label)) {
54
            $errors[] = 'Root Name cannot be empty.';
55
        } elseif (15 < strlen($label)) {
56
            $errors[] = 'Root Name is too long. Max length is 15 characters.';
57
        }
58
59
        $scope = $data['scope'] ?? '';
60
        if (0 === strlen($scope)) {
61
            $errors[] = 'Scope Name cannot be empty.';
62
        } elseif (!preg_match('|^[1-9][0-9]*$|', $scope)) {
63
            $errors[] = 'Scope Name must be integer.';
64
        } elseif (15 < strlen($scope)) {
65
            $errors[] = 'Scope Name is too long. Max length is 15 characters.';
66
        }
67
68
        if (count($errors)) {
69
            throw new ValidationError($errors);
70
        }
71
72
        $data = array(
73
            'name' => $label,
74
        );
75
76
        try {
77
            $this->getTreeAdapter()
78
                ->createRootNode($data, $scope);
79
        } catch (ValidationException $e) {
80
            throw new ValidationError([$e->getMessage()]);
81
        }
82
    }
83
84
    public function createNode(array $data): void
85
    {
86
        $errors = array();
87
88
        $targetId = $_POST['target_node_id'] ?? '';
89
        if (0 === strlen($targetId)) {
90
            $errors[] = 'Target Node cannot be empty.';
91
        }
92
93
        $label = $data['label'] ?? '';
94
        if (0 == strlen($label)) {
95
            $errors[] = 'Name cannot be empty.';
96
        } elseif (15 < strlen($label)) {
97
            $errors[] = 'Name is too long. Max length is 15 characters.';
98
        }
99
100
        $placement = $_POST['placement'] ?? '';
101
        if (0 == strlen($placement)) {
102
            $errors[] = 'Placement cannot be empty.';
103
        }
104
105
        $data = array(
106
            'name' => $label,
107
        );
108
109
        if (count($errors)) {
110
            throw new ValidationError($errors);
111
        }
112
113
        try {
114
            $this->getTreeAdapter()
115
                ->addNode($targetId, $data, $placement);
116
        } catch (ValidationException $e) {
117
            throw new ValidationError([$e->getMessage()]);
118
        }
119
    }
120
121
    public function deleteNode(array $data): void
122
    {
123
        $errors = array();
124
125
        $id = $data['id'] ?? '';
126
127
        if (0 == strlen($id)) {
128
            $errors[] = 'Id is missing. Cannot perform delete operation.';
129
        }
130
131
        if (count($errors)) {
132
            throw new ValidationError($errors);
133
        }
134
135
        $this->getTreeAdapter()
136
            ->deleteBranch($id);
137
    }
138
139
    public function updateNode(array $data): void
140
    {
141
        $errors = array();
142
143
        $nodeId = $_POST['node_id'] ?? '';
144
        if (0 === strlen($nodeId)) {
145
            $errors[] = 'Node cannot be empty.';
146
        }
147
148
        $label = $data['label'] ?? '';
149
        if (0 == strlen($label)) {
150
            $errors[] = 'Name cannot be empty.';
151
        } elseif (15 < strlen($label)) {
152
            $errors[] = 'Name is too long. Max length is 15 characters.';
153
        }
154
155
        $data = array(
156
            'name' => $label,
157
        );
158
159
        if (count($errors)) {
160
            throw new ValidationError($errors);
161
        }
162
163
        try {
164
            $this->getTreeAdapter()
165
                ->updateNode($nodeId, $data);
166
        } catch (ValidationException $e) {
167
            throw new ValidationError([$e->getMessage()]);
168
        }
169
    }
170
171
    public function moveNode(array $data): void
172
    {
173
        $errors = array();
174
175
        $sourceId = $data['source_node_id'] ?? '';
176
        if (0 == strlen($sourceId)) {
177
            $errors[] = 'Source Node cannot be empty.';
178
        }
179
180
        $targetId = $data['target_node_id'] ?? '';
181
        if (0 == strlen($targetId)) {
182
            $errors[] = 'Target Node cannot be empty.';
183
        }
184
185
        $placement = $_POST['placement'] ?? '';
186
        if (0 == strlen($placement)) {
187
            $errors[] = 'Placement cannot be empty.';
188
        }
189
190
        if (count($errors)) {
191
            throw new ValidationError($errors);
192
        }
193
194
        try {
195
            $this->getTreeAdapter()
196
                ->moveNode($sourceId, $targetId, $placement);
197
        } catch (ValidationException $e) {
198
            throw new ValidationError([$e->getMessage()]);
199
        }
200
    }
201
202
    public function getRoots(): array
203
    {
204
        return $this->getTreeAdapter()
205
                    ->getRoots();
206
    }
207
208
    public function getDescendants($nodeId): array
209
    {
210
        return $this->getTreeAdapter()
211
            ->getDescendants($nodeId);
212
    }
213
}
214
215
class ViewHelper
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
216
{
217
    public function escape($string): string
218
    {
219
        return htmlspecialchars((string) $string);
220
    }
221
222
    public function renderTree(array $nodes): string
223
    {
224
        $html = '';
225
226
        $previousLevel = -1;
227
        foreach ($nodes as $node) {
228
            if ($previousLevel > $node['level']) {
229
                for ($i = $node['level']; $previousLevel > $i; ++$i) {
230
                    $html = $html.'</li></ul>';
231
                }
232
                $html = $html.'</li>';
233
            } elseif ($previousLevel < $node['level']) {
234
                $html = $html.'<ul>';
235
            } else {
236
                $html = $html.'</li>';
237
            }
238
239
            $html = $html.'<li>';
240
            $html = $html.'<span>'.$this->escape($node['name']).'</span>'
241
                .' <a href="/?action=delete&id='.$this->escape($node['id']).'" class="badge badge-danger">Delete</a>';
242
243
            $previousLevel = $node['level'];
244
        }
245
246
        for ($i = -1; $previousLevel > $i; ++$i) {
247
            $html = $html.'</li></ul>';
248
        }
249
250
        return $html;
251
    }
252
253
    public function renderSelectOptions(array $nodes): string
254
    {
255
        $pathCache = array();
256
257
        $html = '';
258
259
        foreach ($nodes as $node) {
260
            if (!$node['parent_id']) {
261
                $pathCache[$node['id']] = '/'.$node['name'];
262
            } else {
263
                $pathCache[$node['id']] = $pathCache[$node['parent_id']].'/'.$node['name'];
264
            }
265
            $html .= '<option value="'.$this->escape($node['id']).'">'.$this->escape($pathCache[$node['id']]).'</option>';
266
        }
267
268
        return '<option value="">---</option>'.$html;
269
    }
270
271
    public function renderPlacementOptions(): string
272
    {
273
        $placements = array(
274
            TreeInterface::PLACEMENT_CHILD_TOP => 'Child Top',
275
            TreeInterface::PLACEMENT_CHILD_BOTTOM => 'Child Bottom',
276
            TreeInterface::PLACEMENT_TOP => 'Top',
277
            TreeInterface::PLACEMENT_BOTTOM => 'Bottom',
278
        );
279
280
        $html = '';
281
282
        foreach ($placements as $id => $placement) {
283
            $html .= '<option value="'.$this->escape($id).'">'.$this->escape($placement).'</option>';
284
        }
285
286
        return '<option value="">---</option>'.$html;
287
    }
288
289
    public function renderErrorMessages(array $errors): string
290
    {
291
        $html = '';
292
293
        foreach ($errors as $error) {
294
            $html .= '<li>'.$this->escape($error).'</li>';
295
        }
296
297
        return '<div class="alert alert-danger"><ul class="error-container">'.$html.'</ul></div>';
298
    }
299
300
    public function renderSuccessMessage(string $message): string
301
    {
302
        return '<div class="alert alert-success">'.$this->escape($message).'</div>';
303
    }
304
305
    public function renderFlashMessage(): string
306
    {
307
        $message = $_SESSION['flashMessage'] ?? '';
308
309
        if ($message) {
310
            unset($_SESSION['flashMessage']);
311
312
            return $this->renderSuccessMessage($message);
313
        } else {
314
            return '';
315
        }
316
    }
317
}
318
319
class ValidationError extends \Exception
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
320
{
321
    private $errorMessages = array();
322
323
    public function __construct(array $errorMessages = array())
324
    {
325
        $this->errorMessages = $errorMessages;
326
    }
327
328
    public function addError(string $error): void
329
    {
330
        $this->errorMessages[] = $error;
331
    }
332
333
    public function addErrors(array $errors): void
334
    {
335
        $this->errorMessages = array_merge(array_values($errors), $this->errorMessages);
336
    }
337
338
    public function getErrors(): array
339
    {
340
        return $this->errorMessages;
341
    }
342
}
343
344
function setFlashMessageAndRedirect(string $message, string $url)
345
{
346
    $_SESSION['flashMessage'] = $message;
347
    $redirectUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http')."://$_SERVER[HTTP_HOST]$url";
348
    header(sprintf('Location: %s', $redirectUrl));
349
    die();
350
}
351
352
/************************************
353
 *    Router
354
 ***********************************/
355
$service = new Service($treeAdapter);
356
357
try {
358
    switch ($_GET['action'] ?? '') {
359
        case 'create-scope':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
360
            $service->createRoot($_POST);
361
            setFlashMessageAndRedirect('New root node and scope was successfully created.', '/');
362
            break;
363
        case 'create-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
364
            $service->createNode($_POST);
365
            setFlashMessageAndRedirect('New node was successfully created.', '/');
366
            break;
367
        case 'move-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
368
            $service->moveNode($_POST);
369
            setFlashMessageAndRedirect('Branch/Node was successfully moved.', '/');
370
            break;
371
        case 'update-node':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
372
            $service->updateNode($_POST);
373
            setFlashMessageAndRedirect('Node was successfully updated.', '/');
374
            break;
375
        case 'delete':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
376
            $service->deleteNode($_GET);
377
            setFlashMessageAndRedirect('Branch/Node was successfully deleted.', '/');
378
            break;
379
    }
380
} catch (ValidationError $e) {
381
    $errorMessage = $e->getErrors();
382
}
383
384
/************************************
385
 *   View
386
 ***********************************/
387
$wh = new ViewHelper();
388
?>
389
390
<html>
391
    <head>
392
        <title>Demo</title>
393
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
394
        <style>
395
            .error-container {
396
                margin-bottom: 0;
397
                padding-left: 1rem;
398
            }
399
        </style>
400
    </head>
401
    <body>
402
        <div class="container-fluid">
403
            <h1><a href="/">Demo</a></h1>
404
405
            <?php
406
            if ($errorMessage ?? false) {
407
                echo $wh->renderErrorMessages($errorMessage);
408
            }
409
410
            echo $wh->renderFlashMessage();
411
            ?>
412
413
            <div class="row">
414
                <div class="col-sm-4">
415
                    <form action="/?action=create-scope" method="post">
416
                        <div class="form-group">
417
                            <label>Root name</label>
418
                            <input type="text" name="label" class="form-control" />
419
                        </div>
420
                        <div class="form-group">
421
                            <label>Scope name</label>
422
                            <input type="text" name="scope" class="form-control" />
423
                        </div>
424
                        <input type="submit" value="Create" class="btn btn-primary" />
425
                    </form>
426
                </div>
427
            </div>
428
            <?php
429
            foreach ($service->getRoots() as $root) {
430
                $nodes = $service->getDescendants($root['id']); ?>
431
                <hr />
432
                <h2>Scope - <?php echo $wh->escape($root['group_id']); ?></h2>
433
434
                <div class="row">
435
                    <div class="col-sm-4">
436
                        <h3>Create</h3>
437
                        <form action="/?action=create-node" method="post">
438
                            <div class="form-group">
439
                                <label>Name</label>
440
                                <input type="text" name="label" class="form-control" />
441
                            </div>
442
                            <div class="form-group">
443
                                <label>Target Node</label>
444
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
445
                            </div>
446
                            <div class="form-group">
447
                                <label>Placement</label>
448
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
449
                            </div>
450
                            <div class="form-group">
451
                                <input type="submit" value="Create" class="btn btn-primary" />
452
                            </div>
453
                        </form>
454
                    </div>
455
456
                    <div class="col-sm-4">
457
                        <h3>Move</h3>
458
                        <form action="/?action=move-node" method="post">
459
                            <div class="form-group">
460
                                <label>Source Node</label>
461
                                <select name="source_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
462
                            </div>
463
                            <div class="form-group">
464
                                <label>Target Node</label>
465
                                <select name="target_node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
466
                            </div>
467
                            <div class="form-group">
468
                                <label>Placement</label>
469
                                <select name="placement" class="form-control"><?php echo $wh->renderPlacementOptions(); ?></select>
470
                            </div>
471
                            <input type="submit" value="Move" class="btn btn-primary" />
472
                        </form>
473
                    </div>
474
475
                    <div class="col-sm-4">
476
                        <h3>Update</h3>
477
                        <form action="/?action=update-node" method="post">
478
                            <div class="form-group">
479
                                <label>Name</label>
480
                                <input type="text" name="label" class="form-control" />
481
                            </div>
482
                            <div class="form-group">
483
                                <label>Node</label>
484
                                <select name="node_id" class="form-control"><?php echo $wh->renderSelectOptions($nodes); ?></select>
485
                            </div>
486
                            <input type="submit" value="Update" class="btn btn-primary" />
487
                        </form>
488
                    </div>
489
                </div>
490
                <?php echo $wh->renderTree($nodes); ?>
491
            <?php
492
            }?>
493
        </div>
494
    </body>
495
</html>