Skip to content

Commit c147568

Browse files
committed
Don't handle a property as nullable if a default value is provided and implicitNull is disabled (#33)
Render mixed as type annotation for properties which don't provide type information Fix the output of generated files (contained double slashes for top level classes)
1 parent 86e2a07 commit c147568

File tree

6 files changed

+111
-10
lines changed

6 files changed

+111
-10
lines changed

src/Model/RenderJob.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ public function render(GeneratorConfiguration $generatorConfiguration): void
8686
}
8787

8888
if ($generatorConfiguration->isOutputEnabled()) {
89-
echo "Rendered class {$generatorConfiguration->getNamespacePrefix()}\\$this->classPath\\$this->className\n";
89+
echo sprintf(
90+
"Rendered class %s\n",
91+
join(
92+
'\\',
93+
array_filter([$generatorConfiguration->getNamespacePrefix(), $this->classPath, $this->className])
94+
)
95+
);
9096
}
9197
}
9298

src/SchemaProcessor/PostProcessor/Templates/SetAdditionalProperty.phptpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
public function setAdditionalProperty(
1313
string $property,
14-
{% if validationProperty.getType() %}{% if viewHelper.isPropertyNullable(validationProperty, true) %}?{% endif %}{{ validationProperty.getType() }} {% endif %}$value
14+
{% if validationProperty.getType() %}{% if viewHelper.isPropertyNullable(validationProperty) %}?{% endif %}{{ validationProperty.getType() }} {% endif %}$value
1515
): self {
1616
if (in_array($property, {{ objectProperties }})) {
1717
throw new RegularPropertyAsAdditionalPropertyException($value, $property, self::class);

src/SchemaProcessor/SchemaProcessor.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,16 @@ public function generateClassFile(
210210
): void {
211211
$fileName = join(
212212
DIRECTORY_SEPARATOR,
213-
[$this->destination, str_replace('\\', DIRECTORY_SEPARATOR, $classPath), $className]
213+
array_filter([$this->destination, str_replace('\\', DIRECTORY_SEPARATOR, $classPath), $className])
214214
) . '.php';
215215

216216
$this->renderQueue->addRenderJob(new RenderJob($fileName, $classPath, $className, $schema));
217217

218218
if ($this->generatorConfiguration->isOutputEnabled()) {
219-
echo "Generated class {$this->generatorConfiguration->getNamespacePrefix()}\\$classPath\\$className\n";
219+
echo sprintf(
220+
"Generated class %s\n",
221+
join('\\', array_filter([$this->generatorConfiguration->getNamespacePrefix(), $classPath, $className]))
222+
);
220223
}
221224

222225
$this->generatedFiles[] = $fileName;

src/Templates/Model.phptpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinCl
110110
* {% if viewHelper.getTypeHintAnnotation(property, true) %}@return {{ viewHelper.getTypeHintAnnotation(property, true) }}{% endif %}
111111
*/
112112
public function get{{ viewHelper.ucfirst(property.getAttribute()) }}()
113-
{% if property.getType(true) %}: {% if viewHelper.isPropertyNullable(property) %}?{% endif %}{{ property.getType(true) }}{% endif %}
113+
{% if property.getType(true) %}: {% if viewHelper.isPropertyNullable(property, true) %}?{% endif %}{{ property.getType(true) }}{% endif %}
114114
{
115115
{{ schemaHookResolver.resolveGetterHook(property) }}
116116

@@ -128,7 +128,7 @@ class {{ class }} {% if schema.getInterfaces() %}implements {{ viewHelper.joinCl
128128
* @return self
129129
*/
130130
public function set{{ viewHelper.ucfirst(property.getAttribute()) }}(
131-
{% if property.getType() %}{% if viewHelper.isPropertyNullable(property, true) %}?{% endif %}{{ property.getType() }} {% endif %}${{ property.getAttribute() }}
131+
{% if property.getType() %}{% if viewHelper.isPropertyNullable(property) %}?{% endif %}{{ property.getType() }} {% endif %}${{ property.getAttribute() }}
132132
): self {
133133
if ($this->{{ property.getAttribute() }} === ${{ property.getAttribute() }}) {
134134
return $this;

src/Utils/RenderHelper.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,36 @@ public function validationError(PropertyValidatorInterface $validator): string
111111
return "throw $exceptionConstructor;";
112112
}
113113

114-
public function isPropertyNullable(PropertyInterface $property, bool $setter = false): bool
114+
/**
115+
* check if the property may contain/accept null
116+
* - if the property is required the property may never contain null (if it's a null property null is already
117+
* contained in the proprety type hints)
118+
* - if the output type is requested null may be contained (if the property was not set)
119+
* if implicitNull is enabled null may be set for the property
120+
* - except the property contains a default value and implicit null is disabled. in this case null is not
121+
* possible
122+
*
123+
* @param PropertyInterface $property
124+
* @param bool $outputType
125+
*
126+
* @return bool
127+
*/
128+
public function isPropertyNullable(PropertyInterface $property, bool $outputType = false): bool
115129
{
116-
return (!$setter || $this->generatorConfiguration->isImplicitNullAllowed()) && !$property->isRequired();
130+
return !$property->isRequired()
131+
&& ($outputType || $this->generatorConfiguration->isImplicitNullAllowed())
132+
&& !($property->getDefaultValue() && !$this->generatorConfiguration->isImplicitNullAllowed());
117133
}
118134

119135
public function getTypeHintAnnotation(PropertyInterface $property, bool $outputType = false): string
120136
{
121137
$typeHint = $property->getTypeHint($outputType);
122138

123139
if (!$typeHint) {
124-
return '';
140+
return 'mixed';
125141
}
126142

127-
if (($outputType || $this->generatorConfiguration->isImplicitNullAllowed()) && !$property->isRequired()) {
143+
if ($this->isPropertyNullable($property, $outputType)) {
128144
$typeHint = implode('|', array_unique(array_merge(explode('|', $typeHint), ['null'])));
129145
}
130146

tests/Basic/DefaultValueTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPModelGenerator\Exception\FileSystemException;
66
use PHPModelGenerator\Exception\RenderException;
77
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\GeneratorConfiguration;
89
use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest;
910

1011
/**
@@ -39,6 +40,57 @@ public function testDefaultValueForTypedProperty(string $valueType, $defaultValu
3940
$this->assertSame($object->getProperty(), $compareValue);
4041
}
4142

43+
public function testTypedPropertyTypeHintsWithImplicitNullEnabledAcceptNull(): void
44+
{
45+
$className = $this->generateClassFromFileTemplate(
46+
'DefaultValueTypedProperty.json',
47+
['"integer"', 10],
48+
(new GeneratorConfiguration())->setImmutable(false),
49+
false
50+
);
51+
52+
$object = new $className([]);
53+
54+
$this->assertSame('int|null', $this->getPropertyTypeAnnotation($object, 'property'));
55+
56+
$this->assertSame('int|null', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
57+
$returnType = $this->getReturnType($object, 'getProperty');
58+
$this->assertSame('int', $returnType->getName());
59+
// as implicit null is enabled the default value may be overwritten by a null value
60+
$this->assertTrue($returnType->allowsNull());
61+
62+
$this->assertSame('int|null', $this->getMethodParameterTypeAnnotation($object, 'setProperty'));
63+
$parameterType = $this->getParameterType($object, 'setProperty');
64+
$this->assertSame('int', $parameterType->getName());
65+
// as implicit null is enabled the default value may be overwritten by a null value
66+
$this->assertTrue($parameterType->allowsNull());
67+
}
68+
69+
public function testTypedPropertyTypeHintsWithImplicitNullDisabledDeclinesNull(): void
70+
{
71+
$className = $this->generateClassFromFileTemplate(
72+
'DefaultValueTypedProperty.json',
73+
['"integer"', 10],
74+
(new GeneratorConfiguration())->setImmutable(false),
75+
false,
76+
false
77+
);
78+
79+
$object = new $className([]);
80+
81+
$this->assertSame('int', $this->getPropertyTypeAnnotation($object, 'property'));
82+
83+
$this->assertSame('int', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
84+
$returnType = $this->getReturnType($object, 'getProperty');
85+
$this->assertSame('int', $returnType->getName());
86+
$this->assertFalse($returnType->allowsNull());
87+
88+
$this->assertSame('int', $this->getMethodParameterTypeAnnotation($object, 'setProperty'));
89+
$parameterType = $this->getParameterType($object, 'setProperty');
90+
$this->assertSame('int', $parameterType->getName());
91+
$this->assertFalse($parameterType->allowsNull());
92+
}
93+
4294
public function defaultValueForTypedPropertyDataProvider(): array
4395
{
4496
return [
@@ -151,4 +203,28 @@ public function defaultValueForUntypedPropertyDataProvider(): array
151203
'array' => ['[]', []],
152204
];
153205
}
206+
207+
/**
208+
* @dataProvider implicitNullDataProvider
209+
*/
210+
public function testUntypedPropertyTypeAnnotationsAreMixed(bool $implicitNull): void
211+
{
212+
$className = $this->generateClassFromFileTemplate(
213+
'DefaultValueUntypedProperty.json',
214+
[10],
215+
(new GeneratorConfiguration())->setImmutable(false),
216+
false,
217+
$implicitNull
218+
);
219+
220+
$object = new $className([]);
221+
222+
$this->assertSame('mixed', $this->getPropertyTypeAnnotation($object, 'property'));
223+
224+
$this->assertSame('mixed', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
225+
$this->assertNull($this->getReturnType($object, 'getProperty'));
226+
227+
$this->assertSame('mixed', $this->getMethodParameterTypeAnnotation($object, 'setProperty'));
228+
$this->assertNull($this->getParameterType($object, 'setProperty'));
229+
}
154230
}

0 commit comments

Comments
 (0)