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
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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': |
|
|
|
|
360
|
|
|
$service->createRoot($_POST); |
361
|
|
|
setFlashMessageAndRedirect('New root node and scope was successfully created.', '/'); |
362
|
|
|
break; |
363
|
|
|
case 'create-node': |
|
|
|
|
364
|
|
|
$service->createNode($_POST); |
365
|
|
|
setFlashMessageAndRedirect('New node was successfully created.', '/'); |
366
|
|
|
break; |
367
|
|
|
case 'move-node': |
|
|
|
|
368
|
|
|
$service->moveNode($_POST); |
369
|
|
|
setFlashMessageAndRedirect('Branch/Node was successfully moved.', '/'); |
370
|
|
|
break; |
371
|
|
|
case 'update-node': |
|
|
|
|
372
|
|
|
$service->updateNode($_POST); |
373
|
|
|
setFlashMessageAndRedirect('Node was successfully updated.', '/'); |
374
|
|
|
break; |
375
|
|
|
case 'delete': |
|
|
|
|
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> |
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.