SchemaSnippetsMaker.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Service\Doctrine\SchemaValidation;
  4. use ApiPlatform\Metadata\ApiResource;
  5. use App\Service\Utils\EntityUtils;
  6. use App\Service\Utils\FileUtils;
  7. use App\Service\Utils\Path;
  8. use Doctrine\Common\Collections\Collection;
  9. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  10. use Doctrine\ORM\Mapping\Column;
  11. use Doctrine\ORM\Mapping\Entity;
  12. use Doctrine\ORM\Mapping\InverseJoinColumn;
  13. use Doctrine\ORM\Mapping\JoinColumn;
  14. use Doctrine\ORM\Mapping\JoinTable;
  15. use Doctrine\ORM\Mapping\ManyToMany;
  16. use Doctrine\ORM\Mapping\ManyToOne;
  17. use Doctrine\ORM\Mapping\OneToMany;
  18. use Doctrine\ORM\Mapping\OneToOne;
  19. use Nette\InvalidStateException;
  20. use Nette\PhpGenerator\Attribute;
  21. use Nette\PhpGenerator\ClassType;
  22. use Nette\PhpGenerator\Method;
  23. use Nette\PhpGenerator\Parameter;
  24. use Nette\PhpGenerator\PhpFile;
  25. use Nette\PhpGenerator\PhpNamespace;
  26. use Nette\PhpGenerator\Property;
  27. use Nette\PhpGenerator\PsrPrinter;
  28. /**
  29. * Service produisant les snippets des entités, propriétés et méthodes
  30. * manquantes dans les entités de la V2.
  31. *
  32. * Les résultats sont enregistrés dans le répertoire `~/schema_validation_snippets`.
  33. *
  34. * @see https://github.com/nette/php-generator
  35. */
  36. class SchemaSnippetsMaker
  37. {
  38. public function __construct(
  39. private readonly EntityUtils $entityUtils
  40. )
  41. {}
  42. /**
  43. * Make entities snippets from a 'diff' array,
  44. * as generated by SchemaValidationService::validateSchema()
  45. *
  46. * @param array<string, Difference | array<Difference>> $diff
  47. * @return void
  48. */
  49. public function makeSnippets(array $diff): void
  50. {
  51. $snippetsDir = $this->getSnippetsDir();
  52. $this->prepareSnippetsDir($snippetsDir);
  53. foreach ($diff as $entity => $differences) {
  54. if (empty($differences)) {
  55. continue;
  56. }
  57. $class = $this->makeSnippetEntityClass($entity);
  58. $methods = [];
  59. $collections = [];
  60. $expectedFields = [];
  61. if (!is_array($differences)) {
  62. // New entity
  63. foreach ($differences->getExpectedType() as $field => $type) {
  64. $expectedFields[$field] = $type;
  65. }
  66. } else {
  67. // Existing entity
  68. foreach ($differences as $field => $difference) {
  69. $expectedFields[$field] = $difference->getExpectedType();
  70. }
  71. }
  72. foreach ($expectedFields as $field => $expectedType) {
  73. $prop = is_array($expectedType) ?
  74. $this->makeSnippetEntityCollectionProp($field, $expectedType) :
  75. $this->makeSnippetEntitySimpleProp($field, $expectedType);
  76. $class->addMember($prop);
  77. if ($prop->getType() === Collection::class) {
  78. $collections[] = $prop;
  79. }
  80. $methods = [...$methods, ...$this->makeMethodsSnippetForProp($prop)];
  81. }
  82. if ($collections) {
  83. $class->addMember(
  84. $this->makeSnippetConstructor($collections)
  85. );
  86. }
  87. foreach ($methods as $method) {
  88. $class->addMember($method);
  89. }
  90. $file = $this->makeFileSnippet();
  91. $namespace = $this->makeNamespaceSnippet($entity);
  92. if (is_array($differences)) {
  93. foreach ($this->getEntityToImportFromRelations($differences) as $use) {
  94. $namespace->addUse($use);
  95. }
  96. }
  97. try {
  98. $namespace->add($class);
  99. } catch (InvalidStateException $e) {
  100. var_dump($e->getMessage());
  101. continue;
  102. }
  103. $file->addNamespace($namespace);
  104. $fileName = $this->getSnippetPath($snippetsDir, $entity);
  105. $this->writeSnippet($file, $fileName);
  106. }
  107. }
  108. /**
  109. * Make the getters / setters for the given property
  110. *
  111. * @param Property $prop
  112. * @return array<Method>
  113. */
  114. protected function makeMethodsSnippetForProp(Property $prop): array {
  115. $methods = [];
  116. if ($prop->getType() !== Collection::class) {
  117. $methods[] = $this->makeSnippetGetterForProp($prop);
  118. $methods[] = $this->makeSnippetSetterForProp($prop);
  119. } else {
  120. $methods[] = $this->makeSnippetGetterForProp($prop);
  121. $methods[] = $this->makeSnippetAdderForCollection($prop);
  122. $methods[] = $this->makeSnippetRemoverForCollection($prop);
  123. }
  124. return $methods;
  125. }
  126. /**
  127. * Génère un objet ClassType
  128. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassType.html
  129. *
  130. * @param string $entity
  131. * @return ClassType
  132. */
  133. protected function makeSnippetEntityClass(string $entity): ClassType
  134. {
  135. $class = new ClassType($entity);
  136. $class->setAttributes([
  137. new Attribute(ApiResource::class, ['operations' => []]),
  138. new Attribute(Entity::class, [])
  139. ]);
  140. return $class;
  141. }
  142. /**
  143. * Retourne le chemin absolu vers le répertoire dans lesquels sont créés les snippets
  144. * @return string
  145. */
  146. protected function getSnippetsDir(): string
  147. {
  148. return Path::join(Path::getProjectDir(), 'schema_validation_snippets');
  149. }
  150. /**
  151. * Vide et ajuste les droits du répertoire des snippets
  152. *
  153. * @param string $snippetsDir
  154. * @return void
  155. */
  156. protected function prepareSnippetsDir(string $snippetsDir): void {
  157. if (is_dir($snippetsDir)) {
  158. FileUtils::rrmDir($snippetsDir);
  159. }
  160. mkdir($snippetsDir, 0777, true);
  161. }
  162. /**
  163. * Retourne le chemin absolu du snippet donné
  164. *
  165. * @param string $snippetsDir
  166. * @param string $entity
  167. * @return string
  168. */
  169. protected function getSnippetPath(string $snippetsDir, string $entity): string
  170. {
  171. try {
  172. $fullName = $this->entityUtils->getFullNameFromEntityName($entity);
  173. } catch (\LogicException) {
  174. $fullName = '_NameSpaceNotFound/' . $entity;
  175. }
  176. $relativePath = str_replace('\\', '/', $fullName) . '.php';
  177. return Path::join($snippetsDir, $relativePath);
  178. }
  179. /**
  180. * Créé le fichier du snippet sur le disque
  181. *
  182. * @param PhpFile $phpFile
  183. * @param string $fileName
  184. * @return void
  185. */
  186. protected function writeSnippet(PhpFile $phpFile, string $fileName): void {
  187. if (!is_dir(dirname($fileName))) {
  188. mkdir(dirname($fileName), 0777, true);
  189. }
  190. $printer = new PsrPrinter;
  191. $content = $printer->printFile($phpFile);
  192. $content = $this->postProcessFileContent($content);
  193. $f = fopen($fileName, 'w+');
  194. try {
  195. fwrite($f, $content);
  196. } finally {
  197. fclose($f);
  198. }
  199. }
  200. /**
  201. * Parcourt les propriétés de type relations OneToOne ou ManyToOne pour lister
  202. * les classes d'entités à importer (pour le typage de ces propriétés).
  203. *
  204. * @param array<string, Difference> $differences
  205. * @return array<string>
  206. */
  207. protected function getEntityToImportFromRelations(array $differences): array
  208. {
  209. $imports = [];
  210. foreach ($differences as $field => $difference) {
  211. if (
  212. !is_array($difference->getExpectedType()) ||
  213. !in_array(isset($difference->getExpectedType()['type']), [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE]) ||
  214. !isset($difference->getExpectedType()['targetEntity'])
  215. ) {
  216. continue;
  217. }
  218. $entityName = $this->entityUtils->getEntityNameFromFullName($difference->getExpectedType()['targetEntity']);
  219. if (!$entityName) {
  220. continue;
  221. }
  222. try {
  223. $fullName = $this->entityUtils->getFullNameFromEntityName($entityName);
  224. } catch (\LogicException) {
  225. continue;
  226. }
  227. $imports[] = $fullName;
  228. }
  229. return $imports;
  230. }
  231. /**
  232. * Obtient le namespace pour l'entité donnée
  233. *
  234. * @param string $entity
  235. * @return string
  236. */
  237. protected function getNamespaceValue(string $entity): string
  238. {
  239. try {
  240. $fullQualifiedName = str_contains($entity, "\\") ?
  241. $entity :
  242. $this->entityUtils->getFullNameFromEntityName($entity);
  243. return $this->entityUtils->getNamespaceFromName($fullQualifiedName);
  244. } catch (\LogicException) {
  245. return 'App\\Entity';
  246. }
  247. }
  248. /**
  249. * @param array<string, string|array<string>> $type
  250. * @return string
  251. */
  252. protected function getRelationTargetEntityName(array $type): string
  253. {
  254. $targetEntityName = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']);
  255. try {
  256. return $this->entityUtils->getFullNameFromEntityName($targetEntityName);
  257. } catch (\LogicException) {
  258. return 'mixed';
  259. }
  260. }
  261. /**
  262. * Construit l'objet PhpFile
  263. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpFile.html
  264. *
  265. * @return PhpFile
  266. */
  267. protected function makeFileSnippet(): PhpFile
  268. {
  269. $file = new PhpFile;
  270. $file->addComment('This file is auto-generated.');
  271. $file->setStrictTypes();
  272. return $file;
  273. }
  274. /**
  275. * Construit l'objet PhpNamespace
  276. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpNamespace.html
  277. *
  278. * @param string $entity
  279. * @return PhpNamespace
  280. */
  281. protected function makeNamespaceSnippet(string $entity): PhpNamespace
  282. {
  283. $namespaceValue = $this->getNamespaceValue($entity);
  284. $namespace = new PhpNamespace($namespaceValue);
  285. $namespace->addUse(ApiResource::class);
  286. $namespace->addUse('Doctrine\Common\Collections\ArrayCollection');
  287. $namespace->addUse('Doctrine\Common\Collections\Collection');
  288. $namespace->addUse('Doctrine\ORM\Mapping', 'ORM');
  289. return $namespace;
  290. }
  291. /**
  292. * Construit l'objet Property pour le champs 'id' d'une entité
  293. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
  294. *
  295. * @return Property
  296. */
  297. protected function makeIdPropertySnippet(): Property
  298. {
  299. $prop = new Property('id');
  300. $prop->setPrivate();
  301. $prop->setType('int');
  302. $prop->addAttribute('ORM\Id');
  303. $prop->addAttribute('ORM\Column');
  304. $prop->addAttribute('ORM\GeneratedValue');
  305. return $prop;
  306. }
  307. protected function getPhpTypeFromDoctrineType(string $doctrineType): string
  308. {
  309. return [
  310. 'text' => 'string',
  311. 'boolean' => 'bool',
  312. 'integer' => 'int',
  313. 'datetime' => '?\DateTimeInterface',
  314. 'json_array' => 'array',
  315. ][$doctrineType] ?? 'mixed';
  316. }
  317. /**
  318. * Make a Property object for a simple field (not a relation)
  319. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
  320. *
  321. * @param string $name
  322. * @param string $type
  323. * @return Property
  324. */
  325. protected function makeSnippetEntitySimpleProp(string $name, string $type): Property
  326. {
  327. if ($name === 'id') {
  328. return $this->makeIdPropertySnippet();
  329. }
  330. $php_type = $this->getPhpTypeFromDoctrineType($type);
  331. $prop = new Property($name);
  332. $prop->setProtected();
  333. $prop->setType($php_type);
  334. $prop->setComment("-- Warning : auto-generated property, checkup the attribute options --");
  335. if ($type === 'text') {
  336. $prop->addAttribute(Column::class, ['length' => 255, 'options' => ['nullable' => true]]);
  337. } elseif ($type === 'integer') {
  338. $prop->addAttribute(Column::class, ['type' => 'integer', 'options' => ['nullable' => true]]);
  339. } elseif ($type === 'boolean') {
  340. $prop->addAttribute(Column::class, ['options' => ['default' => false]]);
  341. } elseif ($type === 'datetime') {
  342. $prop->addAttribute(Column::class, ['type' => 'date', 'options' => ['nullable' => true]]);
  343. } elseif ($type === 'json_array') {
  344. $prop->addAttribute(Column::class, ['type' => 'json', 'options' => ['nullable' => true]]);
  345. } else {
  346. $prop->addAttribute(Column::class, []);
  347. }
  348. return $prop;
  349. }
  350. /**
  351. * Make a Property object for a relation field
  352. * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
  353. *
  354. * @param string $name
  355. * @param array<mixed> $type
  356. * @return Property
  357. */
  358. protected function makeSnippetEntityCollectionProp(string $name, array $type): Property
  359. {
  360. $prop = new Property($name);
  361. $prop->setProtected();
  362. $prop->setType('Collection');
  363. if (
  364. isset($type['type']) &&
  365. $type['type'] === ClassMetadataInfo::ONE_TO_ONE || $type['type'] === ClassMetadataInfo::MANY_TO_ONE
  366. ) {
  367. $targetEntityName = $this->getRelationTargetEntityName($type);
  368. $prop->setType($targetEntityName);
  369. } else {
  370. $prop->setType(Collection::class);
  371. }
  372. $attributes = [];
  373. $attributes[] = $this->makeRelationAttribute($type);
  374. $joinTable = $this->makeJoinTableAttributes($type);
  375. if ($joinTable) {
  376. $attributes[] = $joinTable;
  377. }
  378. $attributes = array_merge($attributes, $this->makeJoinColumnAttributes($type));
  379. $attributes = array_merge($attributes, $this->makeJoinColumnAttributes($type, true));
  380. $prop->setAttributes($attributes);
  381. return $prop;
  382. }
  383. /**
  384. * Make the attribute defining the relation (ex: #[ORM\ManyToMany(...)])
  385. *
  386. * @param array<string | array<string | array<string>>> $type
  387. * @return Attribute
  388. */
  389. protected function makeRelationAttribute(array $type): Attribute
  390. {
  391. $options = [];
  392. if (isset($type['mappedBy'])) {
  393. $options['mappedBy'] = $type['mappedBy'];
  394. }
  395. if (isset($type['targetEntity'])) {
  396. $options['targetEntity'] = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']) . '::class';
  397. }
  398. if (isset($type['cascade'])) {
  399. $options['cascade'] = $type['cascade'];
  400. }
  401. if (isset($type['inversedBy'])) {
  402. $options['inversedBy'] = $type['inversedBy'];
  403. }
  404. if (isset($type['orphanRemoval']) && ($type['type'] === ClassMetadataInfo::ONE_TO_MANY || $type['type'] === ClassMetadataInfo::MANY_TO_MANY)) {
  405. $options['orphanRemoval'] = $type['orphanRemoval'];
  406. }
  407. $relationClassNames = [
  408. ClassMetadataInfo::ONE_TO_MANY => OneToMany::class,
  409. ClassMetadataInfo::MANY_TO_MANY => ManyToMany::class,
  410. ClassMetadataInfo::MANY_TO_ONE => ManyToOne::class,
  411. ClassMetadataInfo::ONE_TO_ONE => OneToOne::class,
  412. ];
  413. return new Attribute($relationClassNames[$type['type']], $options);
  414. }
  415. /**
  416. * Make the #[ORM\JoinTable] attribute (if a definition exists)
  417. *
  418. * @param array<string | array<string | array<string>>> $type
  419. * @return Attribute|null
  420. */
  421. protected function makeJoinTableAttributes(array $type): ?Attribute {
  422. if (!isset($type['joinTable'])) {
  423. return null;
  424. }
  425. $options = [];
  426. if (isset($type['joinTable']['name']) && $type['joinTable']['name']) {
  427. $options['name'] = $type['joinTable']['name'];
  428. }
  429. return new Attribute(JoinTable::class, $options);
  430. }
  431. /**
  432. * Make the #[JoinColumn] attributes, if definitions exists.
  433. *
  434. * Is `$inverse` is true, make the #[InverseJoinColumn] instead.
  435. *
  436. * @param array<string | array<string | array<string>>> $type
  437. * @param bool $inverse
  438. * @return array<Attribute>
  439. */
  440. protected function makeJoinColumnAttributes(array $type, bool $inverse = false): array {
  441. $key = $inverse ? 'inverseJoinColumns' : 'joinColumns';
  442. $definition = $type[$key] ?? $type['joinTable'][$key] ?? [];
  443. if (empty($definition)) {
  444. return [];
  445. }
  446. $attributes = [];
  447. foreach ($definition as $joinColDefinition) {
  448. $options = [];
  449. if (isset($joinColDefinition['name']) && $joinColDefinition['name'] !== $type['fieldName'] . '_id') {
  450. $options['name'] = $joinColDefinition['name'];
  451. }
  452. if (($joinColDefinition['unique'] ?? false) === true) {
  453. $options['unique'] = true;
  454. }
  455. if (($joinColDefinition['nullable'] ?? true) === false) {
  456. $options['nullable'] = false;
  457. }
  458. if (($joinColDefinition['onDelete'] ?? null) !== null) {
  459. $options['onDelete'] = $joinColDefinition['onDelete'];
  460. }
  461. if (($joinColDefinition['columnDefinition'] ?? null) !== null) {
  462. $options['columnDefinition'] = $joinColDefinition['columnDefinition'];
  463. }
  464. if (($joinColDefinition['referencedColumnName'] ?? 'id') !== 'id') {
  465. $options['referencedColumnName'] = $joinColDefinition['referencedColumnName'];
  466. }
  467. if (empty($options)) {
  468. // Useless attribute
  469. continue;
  470. }
  471. $attributes[] = new Attribute(
  472. $inverse ? InverseJoinColumn::class : JoinColumn::class,
  473. $options
  474. );
  475. }
  476. return $attributes;
  477. }
  478. /**
  479. * Make the '__construct' method with collections initialization
  480. * @param array<Property> $collections
  481. * @return Method
  482. */
  483. protected function makeSnippetConstructor(array $collections): Method {
  484. $constructor = new Method('__construct');
  485. $constructor->setPublic();
  486. foreach ($collections as $collection) {
  487. $constructor->addBody('$this->' . $collection->getName() . ' = new ArrayCollection();');
  488. }
  489. return $constructor;
  490. }
  491. /**
  492. * Make a 'getter' method for the given property
  493. *
  494. * @param Property $prop
  495. * @return Method
  496. */
  497. protected function makeSnippetGetterForProp(Property $prop): Method {
  498. $method = new Method('get' . ucfirst($prop->getName()));
  499. $method->setReturnType($prop->getType());
  500. $method->setBody('return $this->' . $prop->getName() . ';');
  501. return $method;
  502. }
  503. /**
  504. * Make a 'setter' method for the given property
  505. *
  506. * @param Property $prop
  507. * @return Method
  508. */
  509. protected function makeSnippetSetterForProp(Property $prop): Method {
  510. $method = new Method('set' . ucfirst($prop->getName()));
  511. $parameter = new Parameter($prop->getName());
  512. $parameter->setType($prop->getType());
  513. $method->setParameters([$parameter]);
  514. $method->setReturnType('self');
  515. $method->setBody(
  516. implode(
  517. "\n",
  518. [
  519. '$this->' . $prop->getName() . ' = $' . $prop->getName() . ';',
  520. 'return $this;',
  521. ]
  522. )
  523. );
  524. return $method;
  525. }
  526. protected function getTargetEntityNameFromCollectionProp(Property $prop): ?string
  527. {
  528. if ($prop->getType() !== Collection::class) {
  529. throw new \LogicException('The property must be a collection');
  530. }
  531. foreach ($prop->getAttributes() as $attribute) {
  532. if (
  533. $attribute instanceof Attribute &&
  534. ($attribute->getName() === OneToMany::class || $attribute->getName() === ManyToMany::class)
  535. ) {
  536. $targetEntityName = $attribute->getArguments()['targetEntity'];
  537. if (!$targetEntityName) {
  538. return null;
  539. }
  540. // Normalize result (it could be a FQN, or a '::class' notation)
  541. $targetEntityName = str_replace('::class', '', $targetEntityName);
  542. if (!str_contains($targetEntityName, '\\')) {
  543. try {
  544. $targetEntityName = $this->entityUtils->getFullNameFromEntityName($targetEntityName);
  545. } catch (\LogicException) {
  546. return null;
  547. }
  548. }
  549. return $targetEntityName;
  550. }
  551. }
  552. return null;
  553. }
  554. protected function getInverseSetterCallFromCollectionProp(Property $prop, bool $isRemoving = false): ?string
  555. {
  556. if (
  557. $prop->getType() !== Collection::class
  558. ) {
  559. throw new \LogicException('The property must be a collection');
  560. }
  561. $relationAttr = null;
  562. foreach ($prop->getAttributes() as $attribute) {
  563. if ($attribute->getName() === OneToMany::class || $attribute->getName() === ManyToMany::class) {
  564. $relationAttr = $attribute;
  565. }
  566. }
  567. if (!$relationAttr) {
  568. throw new \LogicException('Missing relation attribute for collection property ' . $prop->getName());
  569. }
  570. $inversedBy = $relationAttr->getArguments()['inversedBy'] ?? $relationAttr->getArguments()['mappedBy'] ?? null;
  571. if (!$inversedBy) {
  572. var_dump('Could not determine the inverse prop for collection property ' . $prop->getName());
  573. $inversedBy = 'XXXX';
  574. }
  575. $attr = $prop->getAttributes()[0];
  576. $prefix = ($attr->getName() === OneToMany::class ? 'set' : ($isRemoving ? 'remove' : 'add'));
  577. return
  578. $prefix .
  579. ucfirst($inversedBy) .
  580. (($prefix === 'set' && $isRemoving) ? '(null)' : '($this)');
  581. }
  582. /**
  583. * Make an 'adder' method for the given property
  584. *
  585. * @param Property $prop
  586. * @return Method
  587. */
  588. protected function makeSnippetAdderForCollection(Property $prop): Method {
  589. $singularPropName = rtrim($prop->getName(), 's');
  590. $method = new Method('add' . ucfirst($singularPropName));
  591. $targetEntityName = $this->getTargetEntityNameFromCollectionProp($prop);
  592. $parameter = new Parameter($singularPropName);
  593. $parameter->setType($targetEntityName ?? 'mixed');
  594. $method->setParameters([$parameter]);
  595. $inverseSetterCall = $this->getInverseSetterCallFromCollectionProp($prop);
  596. $method->setReturnType('self');
  597. $method->setBody(implode(
  598. "\n",
  599. [
  600. 'if (!$this->' . $prop->getName() . '->contains($' . $singularPropName . ')) {',
  601. ' $this->' . $prop->getName() . '[] = $' . $singularPropName . ';',
  602. ' $' . $singularPropName . '->' . $inverseSetterCall . ';',
  603. '}',
  604. '',
  605. 'return $this;',
  606. ]));
  607. return $method;
  608. }
  609. /**
  610. * Make a 'remover' method for the given property
  611. *
  612. * @param Property $prop
  613. * @return Method
  614. */
  615. protected function makeSnippetRemoverForCollection(Property $prop): Method {
  616. $singularPropName = rtrim($prop->getName(), 's');
  617. $method = new Method('remove' . ucfirst($singularPropName));
  618. $targetEntityName = $this->getTargetEntityNameFromCollectionProp($prop);
  619. $parameter = new Parameter($singularPropName);
  620. $parameter->setType($targetEntityName ?? 'mixed');
  621. $method->setParameters([$parameter]);
  622. $inverseSetterCall = $this->getInverseSetterCallFromCollectionProp($prop, true);
  623. $method->setReturnType('self');
  624. $method->setBody(
  625. implode(
  626. "\n",
  627. [
  628. 'if ($this->' . $prop->getName() . '->removeElement($' . $singularPropName . ')) {',
  629. ' $' . $singularPropName . '->' . $inverseSetterCall . ';',
  630. '}',
  631. '',
  632. 'return $this;',
  633. ]
  634. )
  635. );
  636. return $method;
  637. }
  638. /**
  639. * Perform some post-fixes on the file content
  640. *
  641. * @param string $content
  642. * @return string
  643. */
  644. protected function postProcessFileContent(string $content): string
  645. {
  646. return preg_replace("/targetEntity: '(\w+)::class'/", "targetEntity: $1::class", $content);
  647. }
  648. }