1<?php
2
3/*
4 * This file is part of the Prophecy.
5 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
6 *     Marcello Duarte <marcello.duarte@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Prophecy\Doubler;
13
14use Doctrine\Instantiator\Instantiator;
15use Prophecy\Doubler\ClassPatch\ClassPatchInterface;
16use Prophecy\Doubler\Generator\ClassMirror;
17use Prophecy\Doubler\Generator\ClassCreator;
18use Prophecy\Exception\InvalidArgumentException;
19use ReflectionClass;
20
21/**
22 * Cached class doubler.
23 * Prevents mirroring/creation of the same structure twice.
24 *
25 * @author Konstantin Kudryashov <ever.zet@gmail.com>
26 */
27class Doubler
28{
29    private $mirror;
30    private $creator;
31    private $namer;
32
33    /**
34     * @var ClassPatchInterface[]
35     */
36    private $patches = array();
37
38    /**
39     * @var \Doctrine\Instantiator\Instantiator
40     */
41    private $instantiator;
42
43    /**
44     * Initializes doubler.
45     *
46     * @param ClassMirror   $mirror
47     * @param ClassCreator  $creator
48     * @param NameGenerator $namer
49     */
50    public function __construct(ClassMirror $mirror = null, ClassCreator $creator = null,
51                                NameGenerator $namer = null)
52    {
53        $this->mirror  = $mirror  ?: new ClassMirror;
54        $this->creator = $creator ?: new ClassCreator;
55        $this->namer   = $namer   ?: new NameGenerator;
56    }
57
58    /**
59     * Returns list of registered class patches.
60     *
61     * @return ClassPatchInterface[]
62     */
63    public function getClassPatches()
64    {
65        return $this->patches;
66    }
67
68    /**
69     * Registers new class patch.
70     *
71     * @param ClassPatchInterface $patch
72     */
73    public function registerClassPatch(ClassPatchInterface $patch)
74    {
75        $this->patches[] = $patch;
76
77        @usort($this->patches, function (ClassPatchInterface $patch1, ClassPatchInterface $patch2) {
78            return $patch2->getPriority() - $patch1->getPriority();
79        });
80    }
81
82    /**
83     * Creates double from specific class or/and list of interfaces.
84     *
85     * @param ReflectionClass   $class
86     * @param ReflectionClass[] $interfaces Array of ReflectionClass instances
87     * @param array             $args       Constructor arguments
88     *
89     * @return DoubleInterface
90     *
91     * @throws \Prophecy\Exception\InvalidArgumentException
92     */
93    public function double(ReflectionClass $class = null, array $interfaces, array $args = null)
94    {
95        foreach ($interfaces as $interface) {
96            if (!$interface instanceof ReflectionClass) {
97                throw new InvalidArgumentException(sprintf(
98                    "[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n".
99                    "a second argument to `Doubler::double(...)`, but got %s.",
100                    is_object($interface) ? get_class($interface).' class' : gettype($interface)
101                ));
102            }
103        }
104
105        $classname  = $this->createDoubleClass($class, $interfaces);
106        $reflection = new ReflectionClass($classname);
107
108        if (null !== $args) {
109            return $reflection->newInstanceArgs($args);
110        }
111        if ((null === $constructor = $reflection->getConstructor())
112            || ($constructor->isPublic() && !$constructor->isFinal())) {
113            return $reflection->newInstance();
114        }
115
116        if (!$this->instantiator) {
117            $this->instantiator = new Instantiator();
118        }
119
120        return $this->instantiator->instantiate($classname);
121    }
122
123    /**
124     * Creates double class and returns its FQN.
125     *
126     * @param ReflectionClass   $class
127     * @param ReflectionClass[] $interfaces
128     *
129     * @return string
130     */
131    protected function createDoubleClass(ReflectionClass $class = null, array $interfaces)
132    {
133        $name = $this->namer->name($class, $interfaces);
134        $node = $this->mirror->reflect($class, $interfaces);
135
136        foreach ($this->patches as $patch) {
137            if ($patch->supports($node)) {
138                $patch->apply($node);
139            }
140        }
141
142        $this->creator->create($name, $node);
143
144        return $name;
145    }
146}
147