Completed
Push — 1.2 ( d3ea0d...d8c512 )
by David
16:21
created

PhpcrOdmTree::getLabels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $assetHelper can also be of type object<Symfony\Bundle\Fr...ng\Helper\AssetsHelper>. However, the property $assetHelper is declared as type object<Symfony\Component...elper\CoreAssetsHelper>. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
127
        $this->validClasses = $validClasses;
128
129
        $this->depth = $options['depth'];
130
        $this->preciseChildren = $options['precise_children'];
131
132
        if (!$this->assetHelper instanceof CoreAssetsHelper) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Templa...Helper\CoreAssetsHelper does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
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
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Proper...::getPropertyAccessor() has been deprecated with message: since version 2.3, to be removed in 3.0. Use {@link createPropertyAccessor()} instead.

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...
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);
0 ignored issues
show
Bug introduced by
It seems like $parentDocument defined by $this->dm->find(null, $parent) on line 383 can also be of type null; however, Doctrine\ODM\PHPCR\DocumentManager::reorder() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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