Completed
Push — checkout-optimisation ( 40d0de )
by Kamil
21:30
created

AssociationHydrator::hydrateAssociation()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 19
nc 2
nop 2
1
<?php
2
3
namespace Sylius\Bundle\CoreBundle\Doctrine\ORM;
4
5
use Doctrine\Common\Collections\Collection;
6
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
7
use Doctrine\ORM\EntityManager;
8
use Symfony\Component\PropertyAccess\PropertyAccess;
9
use Symfony\Component\PropertyAccess\PropertyAccessor;
10
11
/**
12
 * @author Kamil Kokot <[email protected]>
13
 */
14
final class AssociationHydrator
15
{
16
    /**
17
     * @var EntityManager
18
     */
19
    private $entityManager;
20
21
    /**
22
     * @var ClassMetadata
23
     */
24
    private $classMetadata;
25
26
    /**
27
     * @var PropertyAccessor
28
     */
29
    private $propertyAccessor;
30
31
    /**
32
     * @param EntityManager $entityManager
33
     * @param ClassMetadata $classMetadata
34
     */
35
    public function __construct(EntityManager $entityManager, ClassMetadata $classMetadata)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $entityManager. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
36
    {
37
        $this->entityManager = $entityManager;
38
        $this->classMetadata = $classMetadata;
39
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
40
    }
41
42
    /**
43
     * @param mixed $subjects
44
     * @param array|string[] $associationsPaths
45
     */
46
    public function hydrateAssociations($subjects, array $associationsPaths)
47
    {
48
        foreach ($associationsPaths as $associationPath) {
49
            $this->hydrateAssociation($subjects, $associationPath);
50
        }
51
    }
52
53
    /**
54
     * @param mixed $subjects
55
     * @param string $associationPath
56
     */
57
    public function hydrateAssociation($subjects, $associationPath)
58
    {
59
        $initialAssociations = explode('.', $associationPath);
60
        $finalAssociation = array_pop($initialAssociations);
61
        $subjects = $this->normalizeSubject($subjects);
62
63
        $classMetadata = $this->classMetadata;
64
        foreach ($initialAssociations as $initialAssociation) {
65
            $subjects = array_reduce($subjects, function (array $accumulator, $subject) use ($initialAssociation) {
66
                $subject = $this->propertyAccessor->getValue($subject, $initialAssociation);
67
68
                return array_merge($accumulator, $this->normalizeSubject($subject));
69
            }, []);
70
71
            $classMetadata = $this->entityManager->getClassMetadata($classMetadata->getAssociationTargetClass($initialAssociation));
72
        }
73
74
        $this->entityManager->createQueryBuilder()
75
            ->select('PARTIAL subject.{id}')
76
            ->addSelect('associations')
77
            ->from($classMetadata->name, 'subject')
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
78
            ->leftJoin(sprintf('subject.%s', $finalAssociation), 'associations')
79
            ->where('subject IN (:subjects)')
80
            ->setParameter('subjects', $subjects)
81
            ->getQuery()
82
            ->getResult()
83
        ;
84
    }
85
86
    /**
87
     * @param mixed $subject
88
     *
89
     * @return array
90
     */
91
    private function normalizeSubject($subject)
92
    {
93
        if ($subject instanceof Collection) {
94
            return $subject->toArray();
95
        }
96
97
        if (!is_array($subject)) {
98
            return [$subject];
99
        }
100
101
        return $subject;
102
    }
103
}
104