SchemaSnippetsMaker.php 23 KB

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