1<?php
2/*
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 *
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
18 */
19
20namespace Doctrine\ORM;
21
22use Doctrine\Common\Collections\Criteria;
23use Doctrine\Common\Collections\Selectable;
24use Doctrine\Common\Inflector\Inflector;
25use Doctrine\ORM\Query\ResultSetMappingBuilder;
26use Doctrine\Persistence\ObjectRepository;
27
28/**
29 * An EntityRepository serves as a repository for entities with generic as well as
30 * business specific methods for retrieving entities.
31 *
32 * This class is designed for inheritance and users can subclass this class to
33 * write their own repositories with business-specific methods to locate entities.
34 *
35 * @since   2.0
36 * @author  Benjamin Eberlei <kontakt@beberlei.de>
37 * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
38 * @author  Jonathan Wage <jonwage@gmail.com>
39 * @author  Roman Borschel <roman@code-factory.org>
40 */
41class EntityRepository implements ObjectRepository, Selectable
42{
43    /**
44     * @var string
45     */
46    protected $_entityName;
47
48    /**
49     * @var EntityManager
50     */
51    protected $_em;
52
53    /**
54     * @var \Doctrine\ORM\Mapping\ClassMetadata
55     */
56    protected $_class;
57
58    /**
59     * Initializes a new <tt>EntityRepository</tt>.
60     */
61    public function __construct(EntityManagerInterface $em, Mapping\ClassMetadata $class)
62    {
63        $this->_entityName = $class->name;
64        $this->_em         = $em;
65        $this->_class      = $class;
66    }
67
68    /**
69     * Creates a new QueryBuilder instance that is prepopulated for this entity name.
70     *
71     * @param string $alias
72     * @param string $indexBy The index for the from.
73     *
74     * @return QueryBuilder
75     */
76    public function createQueryBuilder($alias, $indexBy = null)
77    {
78        return $this->_em->createQueryBuilder()
79            ->select($alias)
80            ->from($this->_entityName, $alias, $indexBy);
81    }
82
83    /**
84     * Creates a new result set mapping builder for this entity.
85     *
86     * The column naming strategy is "INCREMENT".
87     *
88     * @param string $alias
89     *
90     * @return ResultSetMappingBuilder
91     */
92    public function createResultSetMappingBuilder($alias)
93    {
94        $rsm = new ResultSetMappingBuilder($this->_em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
95        $rsm->addRootEntityFromClassMetadata($this->_entityName, $alias);
96
97        return $rsm;
98    }
99
100    /**
101     * Creates a new Query instance based on a predefined metadata named query.
102     *
103     * @param string $queryName
104     *
105     * @return Query
106     */
107    public function createNamedQuery($queryName)
108    {
109        return $this->_em->createQuery($this->_class->getNamedQuery($queryName));
110    }
111
112    /**
113     * Creates a native SQL query.
114     *
115     * @param string $queryName
116     *
117     * @return NativeQuery
118     */
119    public function createNativeNamedQuery($queryName)
120    {
121        $queryMapping   = $this->_class->getNamedNativeQuery($queryName);
122        $rsm            = new Query\ResultSetMappingBuilder($this->_em);
123        $rsm->addNamedNativeQueryMapping($this->_class, $queryMapping);
124
125        return $this->_em->createNativeQuery($queryMapping['query'], $rsm);
126    }
127
128    /**
129     * Clears the repository, causing all managed entities to become detached.
130     *
131     * @return void
132     */
133    public function clear()
134    {
135        $this->_em->clear($this->_class->rootEntityName);
136    }
137
138    /**
139     * Finds an entity by its primary key / identifier.
140     *
141     * @param mixed    $id          The identifier.
142     * @param int|null $lockMode    One of the \Doctrine\DBAL\LockMode::* constants
143     *                              or NULL if no specific lock mode should be used
144     *                              during the search.
145     * @param int|null $lockVersion The lock version.
146     *
147     * @return object|null The entity instance or NULL if the entity can not be found.
148     */
149    public function find($id, $lockMode = null, $lockVersion = null)
150    {
151        return $this->_em->find($this->_entityName, $id, $lockMode, $lockVersion);
152    }
153
154    /**
155     * Finds all entities in the repository.
156     *
157     * @return array The entities.
158     */
159    public function findAll()
160    {
161        return $this->findBy([]);
162    }
163
164    /**
165     * Finds entities by a set of criteria.
166     *
167     * @param array      $criteria
168     * @param array|null $orderBy
169     * @param int|null   $limit
170     * @param int|null   $offset
171     *
172     * @return array The objects.
173     */
174    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
175    {
176        $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
177
178        return $persister->loadAll($criteria, $orderBy, $limit, $offset);
179    }
180
181    /**
182     * Finds a single entity by a set of criteria.
183     *
184     * @param array      $criteria
185     * @param array|null $orderBy
186     *
187     * @return object|null The entity instance or NULL if the entity can not be found.
188     */
189    public function findOneBy(array $criteria, array $orderBy = null)
190    {
191        $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
192
193        return $persister->load($criteria, null, null, [], null, 1, $orderBy);
194    }
195
196    /**
197     * Counts entities by a set of criteria.
198     *
199     * @todo Add this method to `ObjectRepository` interface in the next major release
200     *
201     * @param array $criteria
202     *
203     * @return int The cardinality of the objects that match the given criteria.
204     */
205    public function count(array $criteria)
206    {
207        return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->count($criteria);
208    }
209
210    /**
211     * Adds support for magic method calls.
212     *
213     * @param string $method
214     * @param array  $arguments
215     *
216     * @return mixed The returned value from the resolved method.
217     *
218     * @throws ORMException
219     * @throws \BadMethodCallException If the method called is invalid
220     */
221    public function __call($method, $arguments)
222    {
223        if (0 === strpos($method, 'findBy')) {
224            return $this->resolveMagicCall('findBy', substr($method, 6), $arguments);
225        }
226
227        if (0 === strpos($method, 'findOneBy')) {
228            return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments);
229        }
230
231        if (0 === strpos($method, 'countBy')) {
232            return $this->resolveMagicCall('count', substr($method, 7), $arguments);
233        }
234
235        throw new \BadMethodCallException(
236            "Undefined method '$method'. The method name must start with ".
237            "either findBy, findOneBy or countBy!"
238        );
239    }
240
241    /**
242     * @return string
243     */
244    protected function getEntityName()
245    {
246        return $this->_entityName;
247    }
248
249    /**
250     * @return string
251     */
252    public function getClassName()
253    {
254        return $this->getEntityName();
255    }
256
257    /**
258     * @return EntityManager
259     */
260    protected function getEntityManager()
261    {
262        return $this->_em;
263    }
264
265    /**
266     * @return Mapping\ClassMetadata
267     */
268    protected function getClassMetadata()
269    {
270        return $this->_class;
271    }
272
273    /**
274     * Select all elements from a selectable that match the expression and
275     * return a new collection containing these elements.
276     *
277     * @param \Doctrine\Common\Collections\Criteria $criteria
278     *
279     * @return \Doctrine\Common\Collections\Collection
280     */
281    public function matching(Criteria $criteria)
282    {
283        $persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
284
285        return new LazyCriteriaCollection($persister, $criteria);
286    }
287
288    /**
289     * Resolves a magic method call to the proper existent method at `EntityRepository`.
290     *
291     * @param string $method    The method to call
292     * @param string $by        The property name used as condition
293     * @param array  $arguments The arguments to pass at method call
294     *
295     * @throws ORMException If the method called is invalid or the requested field/association does not exist
296     *
297     * @return mixed
298     */
299    private function resolveMagicCall($method, $by, array $arguments)
300    {
301        if (! $arguments) {
302            throw ORMException::findByRequiresParameter($method . $by);
303        }
304
305        $fieldName = lcfirst(Inflector::classify($by));
306
307        if (! ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName))) {
308            throw ORMException::invalidMagicCall($this->_entityName, $fieldName, $method . $by);
309        }
310
311        return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1));
312    }
313}
314