Skip to content

Commit 08d8e8e

Browse files
authored
Merge pull request #85 from Ardenexal/feat/phpstan-model-fixes
feat/phpstan-model-fixes
2 parents 742a051 + cba926e commit 08d8e8e

2,150 files changed

Lines changed: 6873 additions & 1663 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ jobs:
7878
strategy:
7979
fail-fast: false
8080
matrix:
81-
component: [ bundle, codegen, fhir-path, validation ]
81+
component: [ bundle, codegen, fhir-path, validation, models ]
8282

8383
steps:
8484
- uses: actions/checkout@v4

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"phpstan:serialization": "php ./vendor/bin/phpstan.phar analyse src/Component/Serialization/ --configuration=phpstan.neon --memory-limit=-1 -v",
115115
"phpstan:fhir-path": "php ./vendor/bin/phpstan.phar analyse src/Component/FHIRPath/ --configuration=phpstan.neon --memory-limit=-1 -v",
116116
"phpstan:validation": "php ./vendor/bin/phpstan.phar analyse src/Component/Validation/ --configuration=phpstan.neon --memory-limit=-1 -v",
117+
"phpstan:models": "php ./vendor/bin/phpstan.phar analyse src/Component/Models/src/ --configuration=phpstan.neon --memory-limit=-1 -v",
117118
"phpstan-ai": "php scripts/ai-phpstan-runner.php",
118119
"phpstan-ai:bundle": "php scripts/ai-phpstan-runner.php src/Bundle/",
119120
"phpstan-ai:codegen": "php scripts/ai-phpstan-runner.php src/Component/CodeGeneration/",

src/Component/CodeGeneration/src/Generator/FHIRExtensionGenerator.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ private function buildSimpleConstructor(
245245
->setType('array')
246246
->setNullable(false)
247247
->setDefaultValue([]);
248+
$constructor->addComment("@param list<\\Ardenexal\\FHIRTools\\Component\\Models\\{$version}\\DataType\\Extension> \$extension");
248249

249250
$constructor->setBody(
250251
"parent::__construct(\n" .
@@ -275,6 +276,7 @@ private function buildSimpleConstructor(
275276
->setType('array')
276277
->setNullable(false)
277278
->setDefaultValue([]);
279+
$constructor->addComment("@param list<\\Ardenexal\\FHIRTools\\Component\\Models\\{$version}\\DataType\\Extension> \$extension");
278280
$constructor->setBody(
279281
"parent::__construct(\n" .
280282
" id: \$id,\n" .
@@ -349,6 +351,7 @@ private function buildMultiTypeValueConstructor(
349351
->setType('array')
350352
->setNullable(false)
351353
->setDefaultValue([]);
354+
$constructor->addComment("@param list<\\Ardenexal\\FHIRTools\\Component\\Models\\{$version}\\DataType\\Extension> \$extension");
352355
$constructor->setBody(
353356
"parent::__construct(\n" .
354357
" id: \$id,\n" .
@@ -490,9 +493,17 @@ private function buildFromSubExtensionsMethod(
490493
): void {
491494
$slices = $this->collectSlices($elements, $version, $context, $namespace, $errorCollector);
492495

496+
// Match the constructor's required-first parameter order so $args aligns positionally.
497+
usort($slices, static function(array $a, array $b): int {
498+
$aRequired = $a['isRequired'] && !$a['isArray'];
499+
$bRequired = $b['isRequired'] && !$b['isArray'];
500+
501+
return ($bRequired ? 1 : 0) <=> ($aRequired ? 1 : 0);
502+
});
503+
493504
$method = $class->addMethod('fromSubExtensions')
494505
->setStatic(true)
495-
->setReturnType('static')
506+
->setReturnType('self')
496507
->setVisibility('public');
497508

498509
$method->addParameter('subExtensions')->setType('array');
@@ -502,7 +513,7 @@ private function buildFromSubExtensionsMethod(
502513
->setDefaultValue(null);
503514

504515
$method->addComment('Reconstruct from an array of already-denormalized sub-extension objects.');
505-
$method->addComment('@param array<\Ardenexal\FHIRTools\Component\Metadata\Contract\FHIRExtensionInterface> $subExtensions');
516+
$method->addComment("@param array<\\Ardenexal\\FHIRTools\\Component\\Models\\{$version}\\DataType\\Extension> \$subExtensions");
506517
$method->addComment('@param string|null $id');
507518

508519
// Initialise parameter variables
@@ -555,7 +566,7 @@ private function buildFromSubExtensionsMethod(
555566

556567
$args = array_map(static fn (array $s): string => "\${$s['paramName']}", $slices);
557568
$args[] = '$id';
558-
$lines[] = 'return new static(' . implode(', ', $args) . ');';
569+
$lines[] = 'return new self(' . implode(', ', $args) . ');';
559570

560571
$method->setBody(implode("\n", $lines));
561572
}

src/Component/CodeGeneration/src/Generator/FHIRModelGenerator.php

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -388,19 +388,33 @@ public function generateModelClass(array $structureDefinition, string $version,
388388
$class->addMethod('__construct');
389389
$parentParameters = [];
390390

391+
// Build a set of param names the PHP parent constructor actually accepts.
392+
// This prevents passing CanonicalResource-inherited params to DomainResourceResource
393+
// when a FHIR type (e.g. MetadataResource) lists both as ancestors via base.path.
394+
$validParentParamNames = [];
395+
if (isset($parentClass) && $parentClass->class instanceof ClassType) {
396+
try {
397+
$validParentParamNames = array_keys($parentClass->class->getMethod('__construct')->getParameters());
398+
} catch (\Throwable) {
399+
// Parent has no __construct — pass all params as before
400+
}
401+
}
402+
391403
if (isset($structureDefinition['snapshot']) === true) {
392404
$elements = $this->nestElements($structureDefinition['snapshot']['element']);
393405

394406
foreach ($elements['_properties'] as $property) {
395-
$element = $property['_element'];
407+
$element = $property['_element'];
408+
$derivedParam = $this->convertToMethodName($element['base']['path']);
396409
if (
397410
$element['path'] !== $element['base']['path']
398411
&& ! in_array($element['path'], $parentParameters, true)
399412
&& $element['max'] !== '0'
413+
&& ($validParentParamNames === [] || in_array($derivedParam, $validParentParamNames, true))
400414
) {
401-
$parentParameters[] = $this->convertToMethodName($element['base']['path']);
415+
$parentParameters[] = $derivedParam;
402416
}
403-
$this->createForElement($class, $property['_element'], $property['_properties'], $version, $builderContext, $sdUrl);
417+
$this->createForElement($class, $property['_properties'], $version, $builderContext, $sdUrl, $validParentParamNames);
404418
}
405419
}
406420

@@ -409,14 +423,15 @@ public function generateModelClass(array $structureDefinition, string $version,
409423

410424
/**
411425
* @param ClassType $classType
412-
* @param array<string, mixed> $classElement
413426
* @param array<string,array<string, mixed>> $propertyElements
414427
* @param string $version
415428
* @param BuilderContextInterface $builderContext
429+
* @param list<string> $validParentParamNames when non-empty, only elements whose derived parameter
430+
* name appears in this list are added to the parent::__construct() call
416431
*
417432
* @return ClassType
418433
*/
419-
public function createForElement(ClassType $classType, array $classElement, array $propertyElements, string $version, BuilderContextInterface $builderContext, ?string $sdUrl = null): ClassType
434+
public function createForElement(ClassType $classType, array $propertyElements, string $version, BuilderContextInterface $builderContext, ?string $sdUrl = null, array $validParentParamNames = []): ClassType
420435
{
421436
$constructor = $classType->getMethod('__construct');
422437
$parentParameters = [];
@@ -437,14 +452,16 @@ public function createForElement(ClassType $classType, array $classElement, arra
437452
// Track ValueSet dependencies for primitive elements with bindings
438453
$this->trackValueSetDependencies($element, $builderContext);
439454

455+
$derivedParam = $this->convertToMethodName($element['base']['path']);
440456
if (
441457
$element['path'] !== $element['base']['path']
442458
&& ! in_array($element['path'], $parentParameters, true)
443459
&& $element['max'] !== '0'
460+
&& ($validParentParamNames === [] || in_array($derivedParam, $validParentParamNames, true))
444461
) {
445-
$parentParameters[] = $this->convertToMethodName($element['base']['path']);
462+
$parentParameters[] = $derivedParam;
446463
}
447-
$this->addElementAsProperty($propertyElement['_element'], $constructor, $version, $builderContext, $classNamespace);
464+
$this->addElementAsProperty($propertyElement['_element'], $constructor, $version, $builderContext);
448465
} else {
449466
$element = $propertyElement['_element'];
450467

@@ -515,14 +532,16 @@ public function createForElement(ClassType $classType, array $classElement, arra
515532
if (isset($element['definition'])) {
516533
$childClass->addComment('@description ' . $element['definition']);
517534
}
535+
$derivedParam = $this->convertToMethodName($element['base']['path']);
518536
if (
519537
$element['path'] !== $element['base']['path']
520538
&& ! in_array($element['path'], $parentParameters, true)
521539
&& $element['max'] !== '0'
540+
&& ($validParentParamNames === [] || in_array($derivedParam, $validParentParamNames, true))
522541
) {
523-
$parentParameters[] = $this->convertToMethodName($element['base']['path']);
542+
$parentParameters[] = $derivedParam;
524543
}
525-
$this->addElementAsProperty($element, $constructor, $version, $builderContext, $classNamespace);
544+
$this->addElementAsProperty($element, $constructor, $version, $builderContext);
526545

527546
// Emit FHIRPathInvariant attributes on child classes from their element constraints.
528547
foreach ($element['constraint'] ?? [] as $constraint) {
@@ -544,7 +563,7 @@ public function createForElement(ClassType $classType, array $classElement, arra
544563

545564
if (isset($propertyElement['_properties'])) {
546565
// Recursively process nested elements for ValueSet dependencies
547-
$this->createForElement($childClass, $element, $propertyElement['_properties'], $version, $builderContext, $sdUrl);
566+
$this->createForElement($childClass, $propertyElement['_properties'], $version, $builderContext, $sdUrl);
548567
}
549568
}
550569
}
@@ -693,10 +712,9 @@ private function trackProfileBindings(string $profileUrl, BuilderContextInterfac
693712
* @param Method $method
694713
* @param string $version
695714
* @param BuilderContextInterface $builderContext
696-
* @param PhpNamespace $namespace
697715
* @param EnumType|null $enum
698716
*/
699-
private function addElementAsProperty(array $element, Method $method, string $version, BuilderContextInterface $builderContext, PhpNamespace $namespace, ?EnumType $enum = null): void
717+
private function addElementAsProperty(array $element, Method $method, string $version, BuilderContextInterface $builderContext, ?EnumType $enum = null): void
700718
{
701719
$types = [];
702720
if (! isset($element['type']) && isset($element['contentReference'])) {
@@ -711,7 +729,7 @@ private function addElementAsProperty(array $element, Method $method, string $ve
711729
$relatedNamespace = $relatedClass->namespace;
712730
$types[] = '\\' . $relatedNamespace . '\\' . $relatedClass->asClassType()->getName();
713731
} elseif (isset($element['type'])) {
714-
$types = $this->resolveClassFromType($element, $builderContext, $version, $types, $enum, $namespace);
732+
$types = $this->resolveClassFromType($element, $builderContext, $version, $types, $enum);
715733
}
716734

717735
$parameterName = $this->convertToMethodName($element['path']);
@@ -1225,13 +1243,9 @@ private function getNamespaceForFhirType(string $code, string $version, BuilderC
12251243
'ElementDefinition',
12261244
'ProductShelfLife',
12271245
'MarketingStatus',
1228-
];
1229-
1230-
// Types that remain in Resource namespace across all versions
1231-
$typesAlwaysInResource = [
1232-
'SubstanceAmount', // Stays in Resource even in R5
1233-
'ProdCharacteristic', // Stays in Resource even in R5
1234-
'Population', // Stays in Resource even in R5
1246+
'SubstanceAmount', // complex-type in all FHIR versions; generated into DataType
1247+
'ProdCharacteristic', // complex-type in all FHIR versions; generated into DataType
1248+
'Population', // complex-type in all FHIR versions; generated into DataType
12351249
];
12361250

12371251
if (in_array($code, $typesInDataTypeNamespace, true)) {
@@ -1243,11 +1257,6 @@ private function getNamespaceForFhirType(string $code, string $version, BuilderC
12431257
}
12441258
}
12451259

1246-
if (in_array($code, $typesAlwaysInResource, true)) {
1247-
// These types always stay in Resource namespace
1248-
return $builderContext->getElementNamespace($version)->getName();
1249-
}
1250-
12511260
// List of known FHIR primitive types
12521261
$primitiveTypes = [
12531262
'boolean',
@@ -1398,18 +1407,16 @@ private function shouldEmitBindingAttribute(string $bindingStrength): bool
13981407
* Handles ValueSet resolution with versioned URLs, fallback to string type
13991408
* when ValueSet cannot be resolved, and proper enum/code type generation.
14001409
*
1401-
* @param string $valueSetUrl The ValueSet URL (may include version)
1402-
* @param BuilderContextInterface $builderContext The builder context
1403-
* @param string $version The FHIR version
1404-
* @param string $targetElementNamespace The target element namespace
1410+
* @param string $valueSetUrl The ValueSet URL (may include version)
1411+
* @param BuilderContextInterface $builderContext The builder context
1412+
* @param string $version The FHIR version
14051413
*
14061414
* @return string The resolved code type (class name or 'string')
14071415
*/
14081416
private function resolveValueSetCodeType(
14091417
string $valueSetUrl,
14101418
BuilderContextInterface $builderContext,
14111419
string $version,
1412-
string $targetElementNamespace
14131420
): string {
14141421
// Code type wrappers are in DataType namespace since they extend FHIRCode
14151422
$dataTypeNamespace = $builderContext->getDatatypeNamespace($version)->getName();
@@ -1596,17 +1603,15 @@ private function nestElements(array $elements): array
15961603
* @param string $version
15971604
* @param array<string> $types
15981605
* @param EnumType|null $enum
1599-
* @param PhpNamespace $namespace
16001606
*
16011607
* @return array<string>
16021608
*/
1603-
public function resolveClassFromType(array $element, BuilderContextInterface $builderContext, string $version, array $types, ?EnumType $enum, PhpNamespace $namespace): array
1609+
public function resolveClassFromType(array $element, BuilderContextInterface $builderContext, string $version, array $types, ?EnumType $enum): array
16041610
{
16051611
foreach ($element['type'] as $type) {
16061612
$code = $type['code'];
16071613

1608-
$targetElementNamespace = $builderContext->getElementNamespace($version)->getName();
1609-
$targetEnumNamespace = $builderContext->getEnumNamespace($version)->getName();
1614+
$targetEnumNamespace = $builderContext->getEnumNamespace($version)->getName();
16101615
if ($code === 'http://hl7.org/fhirpath/System.String') {
16111616
if (isset($element['base']['path']) && $element['base']['path'] === 'integer.value') {
16121617
$types[] = 'int';
@@ -1696,7 +1701,7 @@ public function resolveClassFromType(array $element, BuilderContextInterface $bu
16961701
// Only generate enums for required binding strength
16971702
if ($this->shouldGenerateEnumForBinding($bindingStrength)) {
16981703
$valueSetUrl = $element['binding']['valueSet'];
1699-
$codeType = $this->resolveValueSetCodeType($valueSetUrl, $builderContext, $version, $targetElementNamespace);
1704+
$codeType = $this->resolveValueSetCodeType($valueSetUrl, $builderContext, $version);
17001705
} else {
17011706
// For extensible, preferred, and example bindings, use string type
17021707
$codeType = 'string';

src/Component/CodeGeneration/tests/Unit/Generator/FHIRExtensionGeneratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public function testFromSubExtensionsBodyChecksSliceUrls(): void
332332
self::assertStringContainsString("'value'", $body, 'fromSubExtensions body should check slice URL "value"');
333333
self::assertStringContainsString("'period'", $body, 'fromSubExtensions body should check slice URL "period"');
334334
self::assertStringContainsString("'comment'", $body, 'fromSubExtensions body should check slice URL "comment"');
335-
self::assertStringContainsString('return new static(', $body, 'fromSubExtensions must return new static(...)');
335+
self::assertStringContainsString('return new self(', $body, 'fromSubExtensions must return new self(...)');
336336
}
337337

338338
public function testFromSubExtensionsBodyUsesInstanceofForClassTypes(): void

src/Component/Metadata/src/Contract/FHIRComplexExtensionInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ interface FHIRComplexExtensionInterface extends FHIRExtensionInterface
3434
* @param array<FHIRExtensionInterface> $subExtensions Denormalized sub-extension objects
3535
* @param string|null $id Optional element id
3636
*/
37-
public static function fromSubExtensions(array $subExtensions, ?string $id = null): static;
37+
public static function fromSubExtensions(array $subExtensions, ?string $id = null): self;
3838
}

src/Component/Models/src/R4/Extension/ADUseExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
#[FHIRExtensionContext(type: 'element', expression: 'Address')]
2222
class ADUseExtension extends Extension
2323
{
24+
/**
25+
* @param list<Extension> $extension
26+
*/
2427
public function __construct(
2528
/** @var CodePrimitive|null valueCode Value of extension */
2629
#[FhirProperty(fhirType: 'code', propertyKind: 'primitive')]

src/Component/Models/src/R4/Extension/ADXPAdditionalLocatorExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
#[FHIRExtensionContext(type: 'element', expression: 'Address.line')]
2222
class ADXPAdditionalLocatorExtension extends Extension
2323
{
24+
/**
25+
* @param list<Extension> $extension
26+
*/
2427
public function __construct(
2528
/** @var StringPrimitive|null valueString Value of extension */
2629
#[FhirProperty(fhirType: 'string', propertyKind: 'primitive')]

src/Component/Models/src/R4/Extension/ADXPBuildingNumberSuffixExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
#[FHIRExtensionContext(type: 'element', expression: 'Address.line')]
2222
class ADXPBuildingNumberSuffixExtension extends Extension
2323
{
24+
/**
25+
* @param list<Extension> $extension
26+
*/
2427
public function __construct(
2528
/** @var StringPrimitive|null valueString Value of extension */
2629
#[FhirProperty(fhirType: 'string', propertyKind: 'primitive')]

src/Component/Models/src/R4/Extension/ADXPCareOfExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
#[FHIRExtensionContext(type: 'element', expression: 'Address.line')]
2222
class ADXPCareOfExtension extends Extension
2323
{
24+
/**
25+
* @param list<Extension> $extension
26+
*/
2427
public function __construct(
2528
/** @var StringPrimitive|null valueString Value of extension */
2629
#[FhirProperty(fhirType: 'string', propertyKind: 'primitive')]

0 commit comments

Comments
 (0)