diff --git a/phpstan.neon b/phpstan.neon index 4d0d6294..53798508 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -61,6 +61,7 @@ parameters: author: false|string, fieldVisibility: 'private'|'protected'|'public'|null, accessorMethods: boolean, + useSimpleArraySetter: boolean, fluentMutatorMethods: boolean, rangeMapping: array, allTypes: boolean, diff --git a/src/Model/Class_.php b/src/Model/Class_.php index d9e87123..c017af9b 100644 --- a/src/Model/Class_.php +++ b/src/Model/Class_.php @@ -218,6 +218,7 @@ public function toNetteFile(array $config, InflectorInterface $inflector, ?PhpFi { $useDoctrineCollections = $config['doctrine']['useCollection']; $useAccessors = $config['accessorMethods']; + $useSimpleArraySetter = $config['useSimpleArraySetter']; $useFluentMutators = $config['fluentMutatorMethods']; $fileHeader = $config['header'] ?? null; $fieldVisibility = $config['fieldVisibility']; @@ -333,7 +334,7 @@ public function toNetteFile(array $config, InflectorInterface $inflector, ?PhpFi foreach ($sortedProperties as $property) { foreach ($property->generateNetteMethods(static function ($string) use ($inflector) { return $inflector->singularize($string)[0]; - }, $namespace, $useDoctrineCollections, $useFluentMutators) as $method) { + }, $namespace, $useDoctrineCollections, $useFluentMutators, $useSimpleArraySetter) as $method) { $methods[] = $method; } } diff --git a/src/Model/Property.php b/src/Model/Property.php index 977414f0..db84f5f2 100644 --- a/src/Model/Property.php +++ b/src/Model/Property.php @@ -81,6 +81,11 @@ public function isArray(): bool return $this->type instanceof ArrayType; } + public function isSimpleArray(): bool + { + return 'array' === $this->typeHint; + } + public function addAnnotation(string $annotation): self { if ('' === $annotation || !\in_array($annotation, $this->annotations, true)) { @@ -154,7 +159,7 @@ public function toNetteProperty(PhpNamespace $namespace, ?string $visibility = n $property->setType($this->resolveName($namespace, $this->typeHint)); } - if (!$this->isArray() || $this->isTypeHintedAsCollection()) { + if (!$this->isArray() || $this->isSimpleArray() || $this->isTypeHintedAsCollection()) { $property->setNullable($this->isNullable); } @@ -195,9 +200,10 @@ public function generateNetteMethods( PhpNamespace $namespace, bool $useDoctrineCollections = true, bool $useFluentMutators = false, + bool $useSimpleArraySetter = false, ): array { return array_merge( - $this->generateMutators($singularize, $namespace, $useDoctrineCollections, $useFluentMutators), + $this->generateMutators($singularize, $namespace, $useDoctrineCollections, $useFluentMutators, $useSimpleArraySetter), $this->isReadable ? [$this->generateGetter($namespace)] : [] ); } @@ -214,7 +220,7 @@ private function generateGetter(PhpNamespace $namespace): Method } if ($this->typeHint) { $getter->setReturnType($this->resolveName($namespace, $this->typeHint)); - if ($this->isNullable && !$this->isArray()) { + if ($this->isNullable && (!$this->isArray() || $this->isSimpleArray())) { $getter->setReturnNullable(); } } @@ -231,13 +237,14 @@ private function generateMutators( PhpNamespace $namespace, bool $useDoctrineCollections = true, bool $useFluentMutators = false, + bool $useSimpleArraySetter = false, ): array { if (!$this->isWritable) { return []; } $mutators = []; - if ($this->isArray()) { + if ($this->isArray() && (!$this->isSimpleArray() || !$useSimpleArraySetter)) { $singularProperty = $singularize($this->name()); $adder = (new Method('add'.ucfirst($singularProperty)))->setVisibility(ClassType::VISIBILITY_PUBLIC); diff --git a/src/SchemaGeneratorConfiguration.php b/src/SchemaGeneratorConfiguration.php index cb512079..bb6db5a1 100644 --- a/src/SchemaGeneratorConfiguration.php +++ b/src/SchemaGeneratorConfiguration.php @@ -167,6 +167,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('author')->defaultFalse()->info('The value of the phpDoc\'s @author annotation')->example('Kévin Dunglas ')->end() ->enumNode('fieldVisibility')->values(['private', 'protected', 'public'])->defaultValue('private')->cannotBeEmpty()->info('Visibility of entities fields')->end() ->booleanNode('accessorMethods')->defaultTrue()->info('Set this flag to false to not generate getter, setter, adder and remover methods')->end() + ->booleanNode('useSimpleArraySetter')->defaultFalse()->info('Set this flag to true to generate setter method for simple arrays instead of adder and remover methods')->end() ->booleanNode('fluentMutatorMethods')->defaultFalse()->info('Set this flag to true to generate fluent setter, adder and remover methods')->end() ->arrayNode('rangeMapping') ->useAttributeAsKey('name') diff --git a/tests/Command/DumpConfigurationTest.php b/tests/Command/DumpConfigurationTest.php index f8ce7614..74f7704d 100644 --- a/tests/Command/DumpConfigurationTest.php +++ b/tests/Command/DumpConfigurationTest.php @@ -154,6 +154,9 @@ interface: App\Model # Example: App\Model # Set this flag to false to not generate getter, setter, adder and remover methods accessorMethods: true + # Set this flag to true to generate setter method for simple arrays instead of adder and remover methods + useSimpleArraySetter: false + # Set this flag to true to generate fluent setter, adder and remover methods fluentMutatorMethods: false rangeMapping: @@ -275,8 +278,7 @@ interface: null generatorTemplates: [] -YAML - , +YAML, $commandTester->getDisplay() ); } diff --git a/tests/Command/ExtractCardinalitiesCommandTest.php b/tests/Command/ExtractCardinalitiesCommandTest.php index 8ef3c115..6eda56e3 100644 --- a/tests/Command/ExtractCardinalitiesCommandTest.php +++ b/tests/Command/ExtractCardinalitiesCommandTest.php @@ -1475,7 +1475,6 @@ public function testExtractCardinalities(): void "https:\/\/schema.org\/mainEntityOfPage": "unknown" } -JSON - , $commandTester->getDisplay()); +JSON, $commandTester->getDisplay()); } } diff --git a/tests/Command/GenerateCommandTest.php b/tests/Command/GenerateCommandTest.php index c9c60a88..f966dbb1 100644 --- a/tests/Command/GenerateCommandTest.php +++ b/tests/Command/GenerateCommandTest.php @@ -70,8 +70,7 @@ public function getFriends(): Collection { return $this->friends; } -PHP - , $person); +PHP, $person); } public function testCustomAttributes(): void @@ -106,29 +105,24 @@ public function testCustomAttributes(): void #[MyAttribute] class Book { -PHP - , $book); +PHP, $book); // Attributes given as unordered map. $this->assertStringContainsString(<<<'PHP' #[ORM\OneToMany(targetEntity: 'App\Entity\Review', mappedBy: 'book', cascade: ['persist', 'remove'])] -PHP - , $book); +PHP, $book); $this->assertStringContainsString(<<<'PHP' #[ORM\OrderBy(name: 'ASC')] -PHP - , $book); +PHP, $book); // Generated attribute could be merged with next one that is a configured one. $this->assertStringContainsString(<<<'PHP' #[ORM\InverseJoinColumn(nullable: false, unique: true, name: 'first_join_column')] -PHP - , $book); +PHP, $book); // Configured attribute could not be merged with next one and it is treated // as repeated. $this->assertStringContainsString(<<<'PHP' #[ORM\InverseJoinColumn(name: 'second_join_column')] -PHP - , $book); +PHP, $book); } public function testFluentMutators(): void @@ -147,8 +141,7 @@ public function setUrl(?string $url): self return $this; } -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function addFriend(Person $friend): self @@ -164,8 +157,7 @@ public function removeFriend(Person $friend): self return $this; } -PHP - , $person); +PHP, $person); } public function testDoNotGenerateAccessorMethods(): void @@ -230,8 +222,7 @@ public function testPropertyDefault(): void $this->assertStringContainsString(<<<'PHP' private string $availability = 'https://schema.org/InStock'; -PHP - , $book); +PHP, $book); } public function testReadableWritable(): void @@ -274,16 +265,14 @@ public function testGeneratedId(): void #[ORM\GeneratedValue(strategy: 'AUTO')] #[ORM\Column(type: 'integer')] private ?int $id = null; -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function getId(): ?int { return $this->id; } -PHP - , $person); +PHP, $person); $this->assertStringNotContainsString('setId(', $person); } @@ -304,24 +293,21 @@ public function testNonGeneratedId(): void #[ORM\Id] #[ORM\Column(type: 'string')] private string $id; -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function getId(): string { return $this->id; } -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function setId(string $id): void { $this->id = $id; } -PHP - , $person); +PHP, $person); } public function testGeneratedUuid(): void @@ -342,16 +328,14 @@ public function testGeneratedUuid(): void #[ORM\Column(type: 'guid')] #[Assert\Uuid] private ?string $id = null; -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function getId(): ?string { return $this->id; } -PHP - , $person); +PHP, $person); $this->assertStringNotContainsString('setId(', $person); } @@ -373,16 +357,14 @@ public function testNonGeneratedUuid(): void #[ORM\Column(type: 'guid')] #[Assert\Uuid] private string $id; -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function getId(): string { return $this->id; } -PHP - , $person); +PHP, $person); $this->assertStringContainsString(<<<'PHP' public function setId(string $id): void @@ -390,8 +372,7 @@ public function setId(string $id): void $this->id = $id; } -PHP - , $person); +PHP, $person); } public function testDoNotGenerateId(): void @@ -463,8 +444,7 @@ public function testGeneratedEnum(): void $this->assertStringContainsString(<<<'PHP' /** @var string The female gender. */ public const FEMALE = 'https://schema.org/Female'; -PHP - , $gender); +PHP, $gender); $this->assertStringNotContainsString('function setId(', $gender); } @@ -490,8 +470,7 @@ public function testSupersededProperties(): void #[ORM\Column(type: 'text', nullable: true)] #[ApiProperty(types: ['https://schema.org/award'])] private ?string $award = null; -PHP - , $creativeWork); +PHP, $creativeWork); $this->assertStringNotContainsString('protected', $creativeWork); } @@ -517,8 +496,7 @@ public function testActivityStreams(): void #[ORM\Column(type: 'text', nullable: true, name: '`content`')] #[ApiProperty(types: ['http://www.w3.org/ns/activitystreams#content'])] private ?string $content = null; -PHP - , $object); +PHP, $object); $page = file_get_contents("$outputDir/App/Entity/Page.php"); @@ -531,13 +509,47 @@ public function testActivityStreams(): void #[ORM\Entity] #[ApiResource(types: ['http://www.w3.org/ns/activitystreams#Page'], routePrefix: 'as')] class Page extends Object_ -PHP - , $page); +PHP, $page); self::assertFalse($this->fs->exists("$outputDir/App/Entity/Delete.php")); self::assertFalse($this->fs->exists("$outputDir/App/Entity/Travel.php")); } + public function testGeneratedSimpleArray(): void + { + $outputDir = __DIR__.'/../../build/simple-array'; + $config = __DIR__.'/../config/simple-array.yaml'; + + $this->fs->mkdir($outputDir); + + $commandTester = new CommandTester(new GenerateCommand()); + $this->assertEquals(0, $commandTester->execute(['output' => $outputDir, 'config' => $config])); + $source = file_get_contents("$outputDir/App/Entity/Project.php"); + + $this->assertStringContainsString(<<<'PHP' + /** + * @see _:shareWith + */ + #[ORM\Column(type: 'simple_array', nullable: true)] + #[Assert\Unique] + private ?array $shareWith = []; +PHP, $source); + + $this->assertStringContainsString(<<<'PHP' + public function setShareWith(?array $shareWith): void + { + $this->shareWith = $shareWith; + } + + public function getShareWith(): ?array + { + return $this->shareWith; + } +PHP, $source); + $this->assertStringNotContainsString('function addShareWith', $source); + $this->assertStringNotContainsString('function removeShareWith', $source); + } + public function testGenerationWithoutConfigFileQuestion(): void { // No config file is given. diff --git a/tests/TypesGeneratorTest.php b/tests/TypesGeneratorTest.php index beb07316..41365e6e 100644 --- a/tests/TypesGeneratorTest.php +++ b/tests/TypesGeneratorTest.php @@ -105,7 +105,7 @@ public function testGenerate(): void $article = file_get_contents("$this->outputDir/App/Entity/Article.php"); $this->assertStringContainsString('abstract class Article extends CreativeWork', $article); $this->assertStringContainsString('private ?string $articleBody = null;', $article); - $this->assertStringContainsString('private array $articleSection = [];', $article); + $this->assertStringContainsString('private ?array $articleSection = [];', $article); $this->assertStringContainsString('public function setArticleBody(?string $articleBody): void', $article); $this->assertStringContainsString('public function getArticleBody(): ?string', $article); $this->assertStringContainsString('public function addArticleSection(string $articleSection): void', $article); diff --git a/tests/config/simple-array.yaml b/tests/config/simple-array.yaml new file mode 100644 index 00000000..add608e6 --- /dev/null +++ b/tests/config/simple-array.yaml @@ -0,0 +1,12 @@ +useSimpleArraySetter: true +types: + Project: + properties: + name: + range: "https://schema.org/Text" + shareWith: + range: "https://schema.org/email" + cardinality: "(0..*)" + attributes: + Assert\Unique: ~ + ORM\Column: { type: "simple_array" } diff --git a/tests/e2e/customized/App/Schema/Enum/GenderType.php b/tests/e2e/customized/App/Schema/Enum/GenderType.php index cbe6dbe4..f2a24993 100644 --- a/tests/e2e/customized/App/Schema/Enum/GenderType.php +++ b/tests/e2e/customized/App/Schema/Enum/GenderType.php @@ -13,9 +13,9 @@ */ class GenderType extends Enum { - /** @var string The male gender. */ - public const MALE = 'https://schema.org/Male'; - /** @var string The female gender. */ public const FEMALE = 'https://schema.org/Female'; + + /** @var string The male gender. */ + public const MALE = 'https://schema.org/Male'; } diff --git a/tests/e2e/original/App/Schema/Entity/Person.php b/tests/e2e/original/App/Schema/Entity/Person.php index 1c5c4682..14fd15b3 100644 --- a/tests/e2e/original/App/Schema/Entity/Person.php +++ b/tests/e2e/original/App/Schema/Entity/Person.php @@ -63,7 +63,7 @@ class Person extends Thing private ?string $additionalName = null; /** - * Gender of something, typically a \[\[Person\]\], but possibly also fictional characters, animals, etc. While https://schema.org/Male and https://schema.org/Female may be used, text strings are also acceptable for people who do not identify as a binary gender. The \[\[gender\]\] property can also be used in an extended sense to cover e.g. the gender of sports teams. As with the gender of individuals, we do not try to enumerate all possibilities. A mixed-gender \[\[SportsTeam\]\] can be indicated with a text value of "Mixed". + * Gender of something, typically a \[\[Person\]\], but possibly also fictional characters, animals, etc. While https://schema.org/Male and https://schema.org/Female may be used, text strings are also acceptable for people who are not a binary gender. The \[\[gender\]\] property can also be used in an extended sense to cover e.g. the gender of sports teams. As with the gender of individuals, we do not try to enumerate all possibilities. A mixed-gender \[\[SportsTeam\]\] can be indicated with a text value of "Mixed". * * @see https://schema.org/gender */ diff --git a/tests/e2e/original/App/Schema/Enum/GenderType.php b/tests/e2e/original/App/Schema/Enum/GenderType.php index cbe6dbe4..f2a24993 100644 --- a/tests/e2e/original/App/Schema/Enum/GenderType.php +++ b/tests/e2e/original/App/Schema/Enum/GenderType.php @@ -13,9 +13,9 @@ */ class GenderType extends Enum { - /** @var string The male gender. */ - public const MALE = 'https://schema.org/Male'; - /** @var string The female gender. */ public const FEMALE = 'https://schema.org/Female'; + + /** @var string The male gender. */ + public const MALE = 'https://schema.org/Male'; }