EntityChoiceList.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
  11. @trigger_error('The '.__NAMESPACE__.'\EntityChoiceList class is deprecated since Symfony 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED);
  12. use Doctrine\Common\Persistence\Mapping\ClassMetadata;
  13. use Doctrine\Common\Persistence\ObjectManager;
  14. use Symfony\Component\Form\Exception\RuntimeException;
  15. use Symfony\Component\Form\Exception\StringCastException;
  16. use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
  17. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  18. /**
  19. * A choice list presenting a list of Doctrine entities as choices.
  20. *
  21. * @author Bernhard Schussek <bschussek@gmail.com>
  22. *
  23. * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
  24. * Use {@link DoctrineChoiceLoader} instead.
  25. */
  26. class EntityChoiceList extends ObjectChoiceList
  27. {
  28. /**
  29. * @var ObjectManager
  30. */
  31. private $em;
  32. /**
  33. * @var string
  34. */
  35. private $class;
  36. /**
  37. * @var ClassMetadata
  38. */
  39. private $classMetadata;
  40. /**
  41. * Metadata for target class of primary key association.
  42. *
  43. * @var ClassMetadata
  44. */
  45. private $idClassMetadata;
  46. /**
  47. * Contains the query builder that builds the query for fetching the
  48. * entities.
  49. *
  50. * This property should only be accessed through queryBuilder.
  51. *
  52. * @var EntityLoaderInterface
  53. */
  54. private $entityLoader;
  55. /**
  56. * The identifier field, if the identifier is not composite.
  57. *
  58. * @var array
  59. */
  60. private $idField = null;
  61. /**
  62. * Whether to use the identifier for index generation.
  63. *
  64. * @var bool
  65. */
  66. private $idAsIndex = false;
  67. /**
  68. * Whether to use the identifier for value generation.
  69. *
  70. * @var bool
  71. */
  72. private $idAsValue = false;
  73. /**
  74. * Whether the entities have already been loaded.
  75. *
  76. * @var bool
  77. */
  78. private $loaded = false;
  79. /**
  80. * The preferred entities.
  81. *
  82. * @var array
  83. */
  84. private $preferredEntities = array();
  85. /**
  86. * Creates a new entity choice list.
  87. *
  88. * @param ObjectManager $manager An EntityManager instance
  89. * @param string $class The class name
  90. * @param string $labelPath The property path used for the label
  91. * @param EntityLoaderInterface $entityLoader An optional query builder
  92. * @param array|\Traversable|null $entities An array of choices or null to lazy load
  93. * @param array $preferredEntities An array of preferred choices
  94. * @param string $groupPath A property path pointing to the property used
  95. * to group the choices. Only allowed if
  96. * the choices are given as flat array.
  97. * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths
  98. */
  99. public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null)
  100. {
  101. $this->em = $manager;
  102. $this->entityLoader = $entityLoader;
  103. $this->classMetadata = $manager->getClassMetadata($class);
  104. $this->class = $this->classMetadata->getName();
  105. $this->loaded = \is_array($entities) || $entities instanceof \Traversable;
  106. $this->preferredEntities = $preferredEntities;
  107. list(
  108. $this->idAsIndex,
  109. $this->idAsValue,
  110. $this->idField
  111. ) = $this->getIdentifierInfoForClass($this->classMetadata);
  112. if (null !== $this->idField && $this->classMetadata->hasAssociation($this->idField)) {
  113. $this->idClassMetadata = $this->em->getClassMetadata(
  114. $this->classMetadata->getAssociationTargetClass($this->idField)
  115. );
  116. list(
  117. $this->idAsIndex,
  118. $this->idAsValue
  119. ) = $this->getIdentifierInfoForClass($this->idClassMetadata);
  120. }
  121. if (!$this->loaded) {
  122. // Make sure the constraints of the parent constructor are
  123. // fulfilled
  124. $entities = array();
  125. }
  126. parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor);
  127. }
  128. /**
  129. * Returns the list of entities.
  130. *
  131. * @return array
  132. *
  133. * @see ChoiceListInterface
  134. */
  135. public function getChoices()
  136. {
  137. if (!$this->loaded) {
  138. $this->load();
  139. }
  140. return parent::getChoices();
  141. }
  142. /**
  143. * Returns the values for the entities.
  144. *
  145. * @return array
  146. *
  147. * @see ChoiceListInterface
  148. */
  149. public function getValues()
  150. {
  151. if (!$this->loaded) {
  152. $this->load();
  153. }
  154. return parent::getValues();
  155. }
  156. /**
  157. * Returns the choice views of the preferred choices as nested array with
  158. * the choice groups as top-level keys.
  159. *
  160. * @return array
  161. *
  162. * @see ChoiceListInterface
  163. */
  164. public function getPreferredViews()
  165. {
  166. if (!$this->loaded) {
  167. $this->load();
  168. }
  169. return parent::getPreferredViews();
  170. }
  171. /**
  172. * Returns the choice views of the choices that are not preferred as nested
  173. * array with the choice groups as top-level keys.
  174. *
  175. * @return array
  176. *
  177. * @see ChoiceListInterface
  178. */
  179. public function getRemainingViews()
  180. {
  181. if (!$this->loaded) {
  182. $this->load();
  183. }
  184. return parent::getRemainingViews();
  185. }
  186. /**
  187. * Returns the entities corresponding to the given values.
  188. *
  189. * @return array
  190. *
  191. * @see ChoiceListInterface
  192. */
  193. public function getChoicesForValues(array $values)
  194. {
  195. // Performance optimization
  196. // Also prevents the generation of "WHERE id IN ()" queries through the
  197. // entity loader. At least with MySQL and on the development machine
  198. // this was tested on, no exception was thrown for such invalid
  199. // statements, consequently no test fails when this code is removed.
  200. // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
  201. if (empty($values)) {
  202. return array();
  203. }
  204. if (!$this->loaded) {
  205. // Optimize performance in case we have an entity loader and
  206. // a single-field identifier
  207. if ($this->idAsValue && $this->entityLoader) {
  208. $unorderedEntities = $this->entityLoader->getEntitiesByIds($this->idField, $values);
  209. $entitiesByValue = array();
  210. $entities = array();
  211. // Maintain order and indices from the given $values
  212. // An alternative approach to the following loop is to add the
  213. // "INDEX BY" clause to the Doctrine query in the loader,
  214. // but I'm not sure whether that's doable in a generic fashion.
  215. foreach ($unorderedEntities as $entity) {
  216. $value = $this->fixValue($this->getSingleIdentifierValue($entity));
  217. $entitiesByValue[$value] = $entity;
  218. }
  219. foreach ($values as $i => $value) {
  220. if (isset($entitiesByValue[$value])) {
  221. $entities[$i] = $entitiesByValue[$value];
  222. }
  223. }
  224. return $entities;
  225. }
  226. $this->load();
  227. }
  228. return parent::getChoicesForValues($values);
  229. }
  230. /**
  231. * Returns the values corresponding to the given entities.
  232. *
  233. * @return array
  234. *
  235. * @see ChoiceListInterface
  236. */
  237. public function getValuesForChoices(array $entities)
  238. {
  239. // Performance optimization
  240. if (empty($entities)) {
  241. return array();
  242. }
  243. if (!$this->loaded) {
  244. // Optimize performance for single-field identifiers. We already
  245. // know that the IDs are used as values
  246. // Attention: This optimization does not check choices for existence
  247. if ($this->idAsValue) {
  248. $values = array();
  249. foreach ($entities as $i => $entity) {
  250. if ($entity instanceof $this->class) {
  251. // Make sure to convert to the right format
  252. $values[$i] = $this->fixValue($this->getSingleIdentifierValue($entity));
  253. }
  254. }
  255. return $values;
  256. }
  257. $this->load();
  258. }
  259. return parent::getValuesForChoices($entities);
  260. }
  261. /**
  262. * Returns the indices corresponding to the given entities.
  263. *
  264. * @param array $entities
  265. *
  266. * @return array
  267. *
  268. * @see ChoiceListInterface
  269. * @deprecated since version 2.4, to be removed in 3.0.
  270. */
  271. public function getIndicesForChoices(array $entities)
  272. {
  273. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.4 and will be removed in 3.0.', E_USER_DEPRECATED);
  274. // Performance optimization
  275. if (empty($entities)) {
  276. return array();
  277. }
  278. if (!$this->loaded) {
  279. // Optimize performance for single-field identifiers. We already
  280. // know that the IDs are used as indices
  281. // Attention: This optimization does not check choices for existence
  282. if ($this->idAsIndex) {
  283. $indices = array();
  284. foreach ($entities as $i => $entity) {
  285. if ($entity instanceof $this->class) {
  286. // Make sure to convert to the right format
  287. $indices[$i] = $this->fixIndex($this->getSingleIdentifierValue($entity));
  288. }
  289. }
  290. return $indices;
  291. }
  292. $this->load();
  293. }
  294. return parent::getIndicesForChoices($entities);
  295. }
  296. /**
  297. * Returns the entities corresponding to the given values.
  298. *
  299. * @param array $values
  300. *
  301. * @return array
  302. *
  303. * @see ChoiceListInterface
  304. * @deprecated since version 2.4, to be removed in 3.0.
  305. */
  306. public function getIndicesForValues(array $values)
  307. {
  308. @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.4 and will be removed in 3.0.', E_USER_DEPRECATED);
  309. // Performance optimization
  310. if (empty($values)) {
  311. return array();
  312. }
  313. if (!$this->loaded) {
  314. // Optimize performance for single-field identifiers.
  315. // Attention: This optimization does not check values for existence
  316. if ($this->idAsIndex && $this->idAsValue) {
  317. return $this->fixIndices($values);
  318. }
  319. $this->load();
  320. }
  321. return parent::getIndicesForValues($values);
  322. }
  323. /**
  324. * Creates a new unique index for this entity.
  325. *
  326. * If the entity has a single-field identifier, this identifier is used.
  327. *
  328. * Otherwise a new integer is generated.
  329. *
  330. * @param mixed $entity The choice to create an index for
  331. *
  332. * @return int|string a unique index containing only ASCII letters,
  333. * digits and underscores
  334. */
  335. protected function createIndex($entity)
  336. {
  337. if ($this->idAsIndex) {
  338. return $this->fixIndex($this->getSingleIdentifierValue($entity));
  339. }
  340. return parent::createIndex($entity);
  341. }
  342. /**
  343. * Creates a new unique value for this entity.
  344. *
  345. * If the entity has a single-field identifier, this identifier is used.
  346. *
  347. * Otherwise a new integer is generated.
  348. *
  349. * @param mixed $entity The choice to create a value for
  350. *
  351. * @return int|string A unique value without character limitations
  352. */
  353. protected function createValue($entity)
  354. {
  355. if ($this->idAsValue) {
  356. return (string) $this->getSingleIdentifierValue($entity);
  357. }
  358. return parent::createValue($entity);
  359. }
  360. /**
  361. * {@inheritdoc}
  362. */
  363. protected function fixIndex($index)
  364. {
  365. $index = parent::fixIndex($index);
  366. // If the ID is a single-field integer identifier, it is used as
  367. // index. Replace any leading minus by underscore to make it a valid
  368. // form name.
  369. if ($this->idAsIndex && $index < 0) {
  370. $index = strtr($index, '-', '_');
  371. }
  372. return $index;
  373. }
  374. /**
  375. * Get identifier information for a class.
  376. *
  377. * @param ClassMetadata $classMetadata The entity metadata
  378. *
  379. * @return array Return an array with idAsIndex, idAsValue and identifier
  380. */
  381. private function getIdentifierInfoForClass(ClassMetadata $classMetadata)
  382. {
  383. $identifier = null;
  384. $idAsIndex = false;
  385. $idAsValue = false;
  386. $identifiers = $classMetadata->getIdentifierFieldNames();
  387. if (1 === \count($identifiers)) {
  388. $identifier = $identifiers[0];
  389. if (!$classMetadata->hasAssociation($identifier)) {
  390. $idAsValue = true;
  391. if (\in_array($classMetadata->getTypeOfField($identifier), array('integer', 'smallint', 'bigint'))) {
  392. $idAsIndex = true;
  393. }
  394. }
  395. }
  396. return array($idAsIndex, $idAsValue, $identifier);
  397. }
  398. /**
  399. * Loads the list with entities.
  400. *
  401. * @throws StringCastException
  402. */
  403. private function load()
  404. {
  405. if ($this->entityLoader) {
  406. $entities = $this->entityLoader->getEntities();
  407. } else {
  408. $entities = $this->em->getRepository($this->class)->findAll();
  409. }
  410. try {
  411. // The second parameter $labels is ignored by ObjectChoiceList
  412. parent::initialize($entities, array(), $this->preferredEntities);
  413. } catch (StringCastException $e) {
  414. throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e);
  415. }
  416. $this->loaded = true;
  417. }
  418. /**
  419. * Returns the first (and only) value of the identifier fields of an entity.
  420. *
  421. * Doctrine must know about this entity, that is, the entity must already
  422. * be persisted or added to the identity map before. Otherwise an
  423. * exception is thrown.
  424. *
  425. * @param object $entity The entity for which to get the identifier
  426. *
  427. * @return array The identifier values
  428. *
  429. * @throws RuntimeException If the entity does not exist in Doctrine's identity map
  430. */
  431. private function getSingleIdentifierValue($entity)
  432. {
  433. $value = current($this->getIdentifierValues($entity));
  434. if ($this->idClassMetadata) {
  435. $class = $this->idClassMetadata->getName();
  436. if ($value instanceof $class) {
  437. $value = current($this->idClassMetadata->getIdentifierValues($value));
  438. }
  439. }
  440. return $value;
  441. }
  442. /**
  443. * Returns the values of the identifier fields of an entity.
  444. *
  445. * Doctrine must know about this entity, that is, the entity must already
  446. * be persisted or added to the identity map before. Otherwise an
  447. * exception is thrown.
  448. *
  449. * @param object $entity The entity for which to get the identifier
  450. *
  451. * @return array The identifier values
  452. *
  453. * @throws RuntimeException If the entity does not exist in Doctrine's identity map
  454. */
  455. private function getIdentifierValues($entity)
  456. {
  457. if (!$this->em->contains($entity)) {
  458. throw new RuntimeException('Entities passed to the choice field must be managed. Maybe persist them in the entity manager?');
  459. }
  460. $this->em->initializeObject($entity);
  461. return $this->classMetadata->getIdentifierValues($entity);
  462. }
  463. }