1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Sonata package. |
5
|
|
|
* |
6
|
|
|
* (c) Thomas Rabaix <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Sonata\DoctrinePHPCRAdminBundle\Tree; |
13
|
|
|
|
14
|
|
|
use Doctrine\ODM\PHPCR\Document\Generic; |
15
|
|
|
use PHPCR\Util\NodeHelper; |
16
|
|
|
|
17
|
|
|
use PHPCR\Util\PathHelper; |
18
|
|
|
use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper; |
19
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccess; |
20
|
|
|
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; |
21
|
|
|
use Symfony\Component\Translation\TranslatorInterface; |
22
|
|
|
use Symfony\Component\Templating\Helper\CoreAssetsHelper; |
23
|
|
|
use Symfony\Cmf\Bundle\TreeBrowserBundle\Tree\TreeInterface; |
24
|
|
|
|
25
|
|
|
use Doctrine\ODM\PHPCR\DocumentManager; |
26
|
|
|
use Doctrine\Common\Util\ClassUtils; |
27
|
|
|
|
28
|
|
|
use Sonata\AdminBundle\Admin\Pool; |
29
|
|
|
use Sonata\AdminBundle\Admin\AdminInterface; |
30
|
|
|
use Sonata\DoctrinePHPCRAdminBundle\Model\ModelManager; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* A tree implementation to work with Doctrine PHPCR-ODM |
34
|
|
|
* |
35
|
|
|
* Your documents need to map all children with an Children mapping for the |
36
|
|
|
* tree to see its children. Not having the Children annotation is a |
37
|
|
|
* possibility to not show children you do not want to show. |
38
|
|
|
* |
39
|
|
|
* @author David Buchmann <[email protected]> |
40
|
|
|
* @author Uwe Jäger <[email protected]> |
41
|
|
|
*/ |
42
|
|
|
class PhpcrOdmTree implements TreeInterface |
43
|
|
|
{ |
44
|
|
|
/** |
45
|
|
|
* @var ModelManager |
46
|
|
|
*/ |
47
|
|
|
private $defaultModelManager; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var DocumentManager |
51
|
|
|
*/ |
52
|
|
|
private $dm; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var Pool |
56
|
|
|
*/ |
57
|
|
|
private $pool; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var TranslatorInterface |
61
|
|
|
*/ |
62
|
|
|
private $translator; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var CoreAssetsHelper |
66
|
|
|
*/ |
67
|
|
|
private $assetHelper; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Array of cached admin services indexed by class name |
71
|
|
|
* @var array |
72
|
|
|
*/ |
73
|
|
|
private $admins = array(); |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* List of the valid class names that may be used as tree "ref" fields |
77
|
|
|
* @var array |
78
|
|
|
*/ |
79
|
|
|
private $validClasses; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Depth to which grand children should be fetched, currently the maximum depth is one |
83
|
|
|
* @var integer |
84
|
|
|
*/ |
85
|
|
|
private $depth; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Fetch children lazy - enabling this will allow the tree to fetch a larger amount of children in the tree but less accurate |
89
|
|
|
* @var bool |
90
|
|
|
*/ |
91
|
|
|
private $preciseChildren; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* The options are |
95
|
|
|
* |
96
|
|
|
* - depth: Down to what level children should be fetched, currently the |
97
|
|
|
* maximum supported depth is one. |
98
|
|
|
* - precise_children: To determine if a tree element has children, check if |
99
|
|
|
* the document has valid children. If false, simply check if the node |
100
|
|
|
* has any child nodes. Less accurate but better performance. |
101
|
|
|
* |
102
|
|
|
* @param DocumentManager $dm |
103
|
|
|
* @param ModelManager $defaultModelManager to use with documents that have no manager |
104
|
|
|
* @param Pool $pool to get admin classes for documents from |
105
|
|
|
* @param TranslatorInterface $translator |
106
|
|
|
* @param CoreAssetsHelper|AssetsHelper $assetHelper |
107
|
|
|
* @param array $validClasses list of the valid class names that may be |
108
|
|
|
* used as tree "ref" fields |
109
|
|
|
* $param integer $depth depth to which grand children should be fetched, |
110
|
|
|
* currently the maximum depth is one |
111
|
|
|
* @param array $options |
112
|
|
|
*/ |
113
|
|
|
public function __construct( |
114
|
|
|
DocumentManager $dm, |
115
|
|
|
ModelManager $defaultModelManager, |
116
|
|
|
Pool $pool, |
117
|
|
|
TranslatorInterface $translator, |
118
|
|
|
$assetHelper, |
119
|
|
|
array $validClasses, |
120
|
|
|
array $options |
121
|
|
|
) { |
122
|
|
|
$this->dm = $dm; |
123
|
|
|
$this->defaultModelManager = $defaultModelManager; |
124
|
|
|
$this->pool = $pool; |
125
|
|
|
$this->translator = $translator; |
126
|
|
|
$this->assetHelper = $assetHelper; |
|
|
|
|
127
|
|
|
$this->validClasses = $validClasses; |
128
|
|
|
|
129
|
|
|
$this->depth = $options['depth']; |
130
|
|
|
$this->preciseChildren = $options['precise_children']; |
131
|
|
|
|
132
|
|
|
if (!$this->assetHelper instanceof CoreAssetsHelper) { |
|
|
|
|
133
|
|
|
// check for the AssetsHelper class introduced in Symfony 2.7 |
134
|
|
|
if (!$this->assetHelper instanceof AssetsHelper) { |
135
|
|
|
throw new \InvalidArgumentException(sprintf( |
136
|
|
|
'The asset helper input for the %s class is not valid.' |
137
|
|
|
.' The current asset helper is an instance of %s.' |
138
|
|
|
.' It should be an instance of one of the following classes: %s', |
139
|
|
|
__CLASS__, |
140
|
|
|
get_class($this->assetHelper), |
141
|
|
|
implode(', ', array( |
142
|
|
|
'Symfony\Component\Templating\Helper\CoreAssetsHelper', |
143
|
|
|
'Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper', |
144
|
|
|
)) |
145
|
|
|
)); |
146
|
|
|
} |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Get the children of the document at this path by looking at the Child and Children mappings. |
152
|
|
|
* |
153
|
|
|
* {@inheritDoc} |
154
|
|
|
*/ |
155
|
|
|
public function getChildren($path) |
156
|
|
|
{ |
157
|
|
|
$root = $this->dm->find(null, $path); |
158
|
|
|
|
159
|
|
|
$children = array(); |
160
|
|
|
|
161
|
|
|
if ($root) { |
162
|
|
|
$rootManager = $this->getModelManager($root); |
163
|
|
|
foreach ($this->getDocumentChildren($rootManager, $root) as $document) { |
164
|
|
|
if ($document instanceof Generic && |
165
|
|
|
(NodeHelper::isSystemItem($document->getNode()) |
166
|
|
|
|| !strncmp('phpcr_locale:', $document->getNode()->getName(), 13) |
167
|
|
|
) |
168
|
|
|
) { |
169
|
|
|
continue; |
170
|
|
|
} |
171
|
|
|
$manager = $this->getModelManager($document); |
172
|
|
|
|
173
|
|
|
$child = $this->documentToArray($manager, $document); |
174
|
|
|
|
175
|
|
|
if ($this->depth > 0) { |
176
|
|
|
foreach ($this->getDocumentChildren($manager, $document) as $grandchild) { |
177
|
|
|
$child['children'][] = $this->documentToArray($manager, $grandchild); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$children[] = $child; |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return $children; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* {@inheritDoc} |
190
|
|
|
*/ |
191
|
|
|
public function move($movedPath, $targetPath) |
192
|
|
|
{ |
193
|
|
|
$resultingPath = $targetPath.'/'.basename($movedPath); |
194
|
|
|
|
195
|
|
|
$document = $this->dm->find(null, $movedPath); |
196
|
|
|
if (null === $document) { |
197
|
|
|
return "No document found at $movedPath"; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
$this->dm->move($document, $resultingPath); |
201
|
|
|
$this->dm->flush(); |
202
|
|
|
|
203
|
|
|
$admin = $this->getAdmin($document); |
204
|
|
|
if (null !== $admin) { |
205
|
|
|
$id = $admin->getNormalizedIdentifier($document); |
206
|
|
|
$urlSafeId = $admin->getUrlsafeIdentifier($document); |
207
|
|
|
} else { |
208
|
|
|
$id = $this->defaultModelManager->getNormalizedIdentifier($document); |
209
|
|
|
$urlSafeId = $this->defaultModelManager->getUrlsafeIdentifier($document); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
return array('id' => $id, 'url_safe_id' => $urlSafeId); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Returns an array representation of the document |
217
|
|
|
* |
218
|
|
|
* @param ModelManager $manager the manager to use with this document |
219
|
|
|
* @param object $document |
220
|
|
|
* |
221
|
|
|
* @return array |
222
|
|
|
*/ |
223
|
|
|
protected function documentToArray(ModelManager $manager, $document) |
224
|
|
|
{ |
225
|
|
|
$className = ClassUtils::getClass($document); |
226
|
|
|
|
227
|
|
|
$rel = (in_array($className, array_keys($this->validClasses))) ? $className : 'undefined'; |
228
|
|
|
$rel = $this->normalizeClassname($rel); |
229
|
|
|
|
230
|
|
|
$admin = $this->getAdmin($document); |
231
|
|
|
if (null !== $admin) { |
232
|
|
|
$label = $admin->toString($document); |
233
|
|
|
$id = $admin->getNormalizedIdentifier($document); |
234
|
|
|
$urlSafeId = $admin->getUrlsafeIdentifier($document); |
235
|
|
|
} else { |
236
|
|
|
$label = method_exists($document, '__toString') ? (string) $document : ClassUtils::getClass($document); |
237
|
|
|
$id = $manager->getNormalizedIdentifier($document); |
238
|
|
|
$urlSafeId = $manager->getUrlsafeIdentifier($document); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if (substr($label, 0, 1) === '/') { |
242
|
|
|
$label = PathHelper::getNodeName($label); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
// TODO: ideally the tree should simply not make the node clickable |
246
|
|
|
$label .= $admin ? '' : ' '.$this->translator->trans('not_editable', array(), 'SonataDoctrinePHPCRAdmin'); |
247
|
|
|
|
248
|
|
|
$hasChildren = false; |
249
|
|
|
if (isset($this->validClasses[$className]['valid_children']) |
250
|
|
|
&& count($this->validClasses[$className]['valid_children']) |
251
|
|
|
) { |
252
|
|
|
if ($this->preciseChildren) { |
253
|
|
|
// determine if a node has children the accurate way. we need to |
254
|
|
|
// loop over all documents, as a PHPCR node might have children but |
255
|
|
|
// only invalid ones. this is quite costly. |
256
|
|
|
$hasChildren = (bool) count($this->getDocumentChildren($manager, $document)); |
257
|
|
|
} else { |
258
|
|
|
// just check if there is any child node |
259
|
|
|
$hasChildren = $manager->getDocumentManager()->getNodeForDocument($document)->hasNodes(); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return array( |
264
|
|
|
'data' => $label, |
265
|
|
|
'attr' => array( |
266
|
|
|
'id' => $id, |
267
|
|
|
'url_safe_id' => $urlSafeId, |
268
|
|
|
'rel' => $rel |
269
|
|
|
), |
270
|
|
|
'state' => $hasChildren ? 'closed' : null, |
271
|
|
|
); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* @param object $document the PHPCR-ODM document to get the sonata admin for |
276
|
|
|
* |
277
|
|
|
* @return AdminInterface |
278
|
|
|
*/ |
279
|
|
|
private function getAdmin($document) |
280
|
|
|
{ |
281
|
|
|
$className = ClassUtils::getClass($document); |
282
|
|
|
return $this->getAdminByClass($className); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param string $className |
287
|
|
|
* |
288
|
|
|
* @return AdminInterface |
289
|
|
|
*/ |
290
|
|
|
private function getAdminByClass($className) |
291
|
|
|
{ |
292
|
|
|
if (!isset($this->admins[$className])) { |
293
|
|
|
// will return null if not defined |
294
|
|
|
$this->admins[$className] = $this->pool->getAdminByClass($className); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
return $this->admins[$className]; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* @param ModelManager $manager the manager to use with this document |
302
|
|
|
* @param object $document the PHPCR-ODM document to get the children of |
303
|
|
|
* |
304
|
|
|
* @return array of children indexed by child nodename pointing to the child documents |
305
|
|
|
*/ |
306
|
|
|
private function getDocumentChildren(ModelManager $manager, $document) |
307
|
|
|
{ |
308
|
|
|
$accessor = PropertyAccess::getPropertyAccessor(); // use deprecated BC method to support symfony 2.2 |
|
|
|
|
309
|
|
|
|
310
|
|
|
/** @var $meta \Doctrine\ODM\PHPCR\Mapping\ClassMetadata */ |
311
|
|
|
$meta = $manager->getMetadata(ClassUtils::getClass($document)); |
312
|
|
|
|
313
|
|
|
$children = array(); |
314
|
|
|
foreach ($meta->childrenMappings as $fieldName) { |
315
|
|
|
try { |
316
|
|
|
$prop = $accessor->getValue($document, $fieldName); |
317
|
|
|
} catch (NoSuchPropertyException $e) { |
318
|
|
|
$prop = $meta->getReflectionProperty($fieldName)->getValue($document); |
319
|
|
|
} |
320
|
|
|
if (null === $prop) { |
321
|
|
|
continue; |
322
|
|
|
} |
323
|
|
|
if (!is_array($prop)) { |
324
|
|
|
$prop = $prop->toArray(); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
$children = array_merge($children, $this->filterDocumentChildren($document, $prop)); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
foreach ($meta->childMappings as $fieldName) { |
331
|
|
|
try { |
332
|
|
|
$prop = $accessor->getValue($document, $fieldName); |
333
|
|
|
} catch (NoSuchPropertyException $e) { |
334
|
|
|
$prop = $meta->getReflectionProperty($fieldName)->getValue($document); |
335
|
|
|
} |
336
|
|
|
if (null !== $prop && $this->isValidDocumentChild($document, $prop)) { |
337
|
|
|
$children[$fieldName] = $prop; |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return $children; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* @param object $document |
346
|
|
|
* @param array $children |
347
|
|
|
* |
348
|
|
|
* @return array of valid children for the document |
349
|
|
|
*/ |
350
|
|
|
protected function filterDocumentChildren($document, array $children) |
351
|
|
|
{ |
352
|
|
|
$me = $this; |
353
|
|
|
|
354
|
|
|
return array_filter($children, function ($child) use ($me, $document) { |
355
|
|
|
return $me->isValidDocumentChild($document, $child); |
356
|
|
|
}); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @param object $document |
361
|
|
|
* @param object $child |
362
|
|
|
* |
363
|
|
|
* @return boolean TRUE if valid, FALSE if not valid |
364
|
|
|
*/ |
365
|
|
|
public function isValidDocumentChild($document, $child) |
366
|
|
|
{ |
367
|
|
|
$className = ClassUtils::getClass($document); |
368
|
|
|
$childClassName = ClassUtils::getClass($child); |
369
|
|
|
|
370
|
|
|
if (!isset($this->validClasses[$className])) { |
371
|
|
|
// no mapping means no valid children |
372
|
|
|
return false; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
return in_array($childClassName, $this->validClasses[$className]['valid_children']); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* {@inheritDoc} |
380
|
|
|
*/ |
381
|
|
|
public function reorder($parent, $moved, $target, $before) |
382
|
|
|
{ |
383
|
|
|
$parentDocument = $this->dm->find(null, $parent); |
384
|
|
|
$this->dm->reorder($parentDocument, basename($moved), basename($target), $before); |
|
|
|
|
385
|
|
|
$this->dm->flush(); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* {@inheritDoc} |
390
|
|
|
*/ |
391
|
|
|
public function getAlias() |
392
|
|
|
{ |
393
|
|
|
return 'phpcr_odm_tree'; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* {@inheritDoc} |
398
|
|
|
*/ |
399
|
|
|
public function getNodeTypes() |
400
|
|
|
{ |
401
|
|
|
$result = array(); |
402
|
|
|
|
403
|
|
|
foreach ($this->validClasses as $className => $children) { |
404
|
|
|
$rel = $this->normalizeClassname($className); |
405
|
|
|
$admin = $this->getAdminByClass($className); |
406
|
|
|
$validChildren = array(); |
407
|
|
|
|
408
|
|
|
foreach ($children['valid_children'] as $child) { |
409
|
|
|
$validChildren[] = $this->normalizeClassname($child); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
$icon = 'bundles/cmftreebrowser/images/folder.png'; |
413
|
|
|
if (!empty($children['image'])) { |
414
|
|
|
$icon = $children['image']; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
$routes = array(); |
418
|
|
|
if (null !== $admin) { |
419
|
|
|
foreach ($admin->getRoutes()->getElements() as $code => $route) { |
420
|
|
|
$action = explode('.', $code); |
421
|
|
|
$key = $this->mapAction(end($action)); |
422
|
|
|
|
423
|
|
|
if (null !== $key) { |
424
|
|
|
$routes[$key] = sprintf('%s_%s', $admin->getBaseRouteName(), end($action)); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
$result[$rel] = array( |
430
|
|
|
'icon' => array('image' => $this->assetHelper->getUrl($icon)), |
431
|
|
|
'label' => (null !== $admin) ? $admin->trans($admin->getLabel()) : $className, |
432
|
|
|
'valid_children' => $validChildren, |
433
|
|
|
'routes' => $routes |
434
|
|
|
); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
return $result; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* {@inheritDoc} |
442
|
|
|
*/ |
443
|
|
|
public function getLabels() |
444
|
|
|
{ |
445
|
|
|
return array( |
446
|
|
|
'createItem' => $this->translator->trans('create_item', array(), 'SonataDoctrinePHPCRAdmin'), |
447
|
|
|
'deleteItem' => $this->translator->trans('delete_item', array(), 'SonataDoctrinePHPCRAdmin'), |
448
|
|
|
); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* @param string $className |
453
|
|
|
* |
454
|
|
|
* @return string |
455
|
|
|
*/ |
456
|
|
|
private function normalizeClassname($className) |
457
|
|
|
{ |
458
|
|
|
return str_replace('\\', '_', $className); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* @param string $action |
463
|
|
|
* |
464
|
|
|
* @return string|null |
465
|
|
|
*/ |
466
|
|
|
private function mapAction($action) |
467
|
|
|
{ |
468
|
|
|
switch ($action) { |
469
|
|
|
case 'edit': return 'select_route'; |
470
|
|
|
case 'create': return 'create_route'; |
471
|
|
|
case 'delete': return 'delete_route'; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return null; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* @param object $document |
479
|
|
|
* |
480
|
|
|
* @return ModelManager the modelmanager for $document or the default manager |
481
|
|
|
*/ |
482
|
|
|
protected function getModelManager($document = NULL) |
483
|
|
|
{ |
484
|
|
|
$admin = $document ? $this->getAdmin($document) : NULL; |
485
|
|
|
|
486
|
|
|
return $admin ? $admin->getModelManager() : $this->defaultModelManager; |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.