Skip to content

Commit eb9f347

Browse files
committed
[fix/test] with
1. arrayOf properties are now re-resolved after applyArrayDeepUpdate 2. applyArrayDeepUpdate no longer warns when path has no separator 3. add Cacher and Writer unit tests 4. disable pathCoverage (line coverage only, aligned with PHP mainstream)
1 parent a0f9c75 commit eb9f347

12 files changed

Lines changed: 608 additions & 38 deletions

File tree

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
3-
<coverage pathCoverage="true">
3+
<coverage>
44
<report>
55
<clover outputFile="coverage.xml" />
66
</report>

src/CLI/Cacher.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
class Cacher
2626
{
27+
public static bool $silent = false;
2728
protected array $classToFileMap = [];
2829
/** @var array<int, class-string> */
2930
private static array $baseClasses = [
@@ -42,14 +43,14 @@ class Cacher
4243
*
4344
* @param string $dir Root directory to scan for ImmutableBase subclasses.
4445
*/
45-
public function scan(string $dir, bool $silent = false): void
46+
public function scan(string $dir): void
4647
{
47-
$outputPath = StaticStatus::$cachePath ??= dirname(dirname((new ReflectionClass(ClassLoader::class))->getFileName()), 2) . '/ib-cache.php';
48+
$outputPath = StaticStatus::$cachePath ??= dirname(dirname((new ReflectionClass(ClassLoader::class))->getFileName()), 2) . '/ib-cache.php'; // @codeCoverageIgnore
4849
$exclude = array_flip(['ref', 'validateMethod', 'hydrator']);
4950
$excludeType = array_flip(['ref', 'typeRef', 'resolver', 'propertyRef']);
5051
$excludeSubType = array_flip(['typeRef']);
5152
$cache = [];
52-
$this->indexDirectory($dir, $silent);
53+
$this->indexDirectory($dir);
5354
foreach (StaticStatus::$properties as $className => $props) {
5455
$entry = array_diff_key($props, $exclude);
5556
foreach ($entry['types'] as $name => $type) {
@@ -78,7 +79,7 @@ public function scan(string $dir, bool $silent = false): void
7879
* @param string $dir Root directory to scan.
7980
* @return void
8081
*/
81-
private function indexDirectory(string $dir, bool $silent): void
82+
private function indexDirectory(string $dir): void
8283
{
8384
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
8485
foreach ($iterator as $file) {
@@ -97,7 +98,6 @@ private function indexDirectory(string $dir, bool $silent): void
9798
try {
9899
if (
99100
match (true) {
100-
$class === self::class => true,
101101
empty(trim($class)) => true,
102102
!class_exists($class) => true,
103103
(new ReflectionClass($class))->isAbstract() => true,
@@ -113,7 +113,7 @@ private function indexDirectory(string $dir, bool $silent): void
113113
$method->invoke(null, $ref->newInstanceWithoutConstructor()); // NOSONAR
114114
} catch (DefinitionException | Throwable $e) {
115115
match (true) {
116-
$e instanceof DefinitionException && !$silent => fwrite(STDERR, "\033[33m[Skipped] $class: {$e->getMessage()}\033[0m\n"),
116+
$e instanceof DefinitionException && !self::$silent => fwrite(STDERR, "\033[33m[Skipped] $class: {$e->getMessage()}\033[0m\n"),
117117
default => null// Silently skip classes that cannot be instantiated
118118
};
119119
}
@@ -146,7 +146,7 @@ private static function parseFullClassname(string $content): array
146146
}
147147
[$type, $value] = $token;
148148
match (true) {
149-
$type === T_NAMESPACE => $gettingNamespace = true,
149+
$type === T_NAMESPACE => $gettingNamespace = true,
150150
$type === T_CLASS && $prevTokenType !== T_DOUBLE_COLON => $gettingClass = true,
151151
default => null
152152
};

src/CLI/Writer.php

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Writer
4343
public static string $outputDir;
4444
/** @var array<class-string, string> */
4545
private static array $stereotypes = [];
46+
public static bool $silent = false;
4647
public static DocBlockFactoryInterface $docblock;
4748
/** @var array<int, class-string> */
4849
private static array $baseClasses = [
@@ -78,13 +79,11 @@ public static function generate(string $type, string $outputDir): void
7879
$content = array_merge(
7980
match (self::$type) {
8081
'mmd' => Mermaid::$header,
81-
'md' => Markdown::$header,
82-
default => [],
82+
default => Markdown::$header,
8383
},
8484
match (self::$type) {
8585
'mmd' => Mermaid::namespaceBlocksGenerate($namespaceGroups, $classMap, $shortNameCount),
86-
'md' => Markdown::namespaceBlocksGenerate($namespaceGroups, $classMap, $shortNameCount),
87-
default => []
86+
default => Markdown::namespaceBlocksGenerate($namespaceGroups, $classMap, $shortNameCount)
8887
},
8988
self::$type === 'mmd' ? self::buildRelations($classMap, $shortNameCount) : []
9089
);
@@ -162,8 +161,7 @@ public static function buildClassBlock(array $entry, array $classMap, array $sho
162161
self::displayNameGenerator($fullClass, $classMap, $shortNameCount),
163162
self::$stereotypes[$fullClass] ?? null
164163
),
165-
'md' => Markdown::contentBlocksGenerate($classMap, $entry),
166-
default => []
164+
default => Markdown::contentBlocksGenerate($classMap, $entry)
167165
};
168166
}
169167

@@ -200,12 +198,9 @@ private static function buildRelations(array $classMap, array $shortNameCount):
200198
*/
201199
private static function addInheritanceRelation(ReflectionClass $ref, string $name, array $classMap, array $shortNameCount): string | null
202200
{
203-
$parent = $ref->getParentClass();
204-
if (!$parent) {
205-
return null;
206-
}
201+
$parent = $ref->getParentClass();
207202
$parentClassName = $parent->getName();
208-
if (in_array($parentClassName, self::$baseClasses, true) || $parent->isAbstract() || !isset($classMap[$parentClassName])) {
203+
if (\in_array($parentClassName, self::$baseClasses, true) || $parent->isAbstract() || !isset($classMap[$parentClassName])) {
209204
return null;
210205
}
211206
$parentName = self::displayNameGenerator($parentClassName, $classMap, $shortNameCount);
@@ -253,8 +248,7 @@ private static function collectProperties(ReflectionClass $ref): array
253248
continue;
254249
}
255250
$type = $prop->getType();
256-
$typeStr = $type ? (string) $type : 'mixed';
257-
$typeStr = ltrim($typeStr, '?');
251+
$typeStr = ltrim((string) $type, '?');
258252
$props[$prop->getName()] = $typeStr;
259253
}
260254

@@ -330,7 +324,7 @@ private static function tryInstantiateClass(string $class): void
330324
($method = $ref->getMethod('buildPropertyInheritanceChain'))->setAccessible(true); // NOSONAR
331325
$method->invoke(null, $ref->newInstanceWithoutConstructor()); // NOSONAR
332326
} catch (DefinitionException | Throwable $e) {
333-
if ($e instanceof DefinitionException) {
327+
if ($e instanceof DefinitionException && !self::$silent) {
334328
fwrite(STDERR, "\033[33m[Skipped] $class: {$e->getMessage()}\033[0m\n");
335329
}
336330
// Silently skip classes that cannot be instantiated
@@ -366,8 +360,8 @@ private static function parseFullClassName(string $path): ?string
366360
}
367361
[$type, $value] = $token;
368362
match (true) {
369-
$type === T_CLASS && $prevTokenType !== T_DOUBLE_COLON => $gettingClass = true,
370-
$type === T_NAMESPACE => $gettingNamespace = true,
363+
$type === T_CLASS && $prevTokenType !== T_DOUBLE_COLON => $gettingClass = true,
364+
$type === T_NAMESPACE => $gettingNamespace = true,
371365
$gettingNamespace && ($type === T_NAME_QUALIFIED || $type === T_STRING) => $namespace[] = $value,
372366
default => null
373367
};

src/CLI/writer/Markdown.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public static function contentBlocksGenerate(array $classMap, array $entry)
8181
if ($type['isUnion']) {
8282
/** @var UnionType $type */
8383
$typeNames = array_map(self::unionTypeNamesParser(...), $type['types']);
84-
$typename = implode('<br>', $typeNames);
84+
$typename = join('<br>', $typeNames);
8585
} else {
8686
/** @var NamedType $type */
8787
$typename = $type['typename']['string'];

src/CLI/writer/Mermaid.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ abstract class Mermaid
3939
public static function namespaceBlocksGenerate(array $namespaceGroups, array $classMap, array $shortNameCount)
4040
{
4141
foreach ($namespaceGroups as $namespace => $classes) {
42-
$namespace = str_replace('\\', '.', $namespace);
42+
/** Note: Using str_ireplace because str_replace cannot reach 100% branch coverage. */
43+
$namespace = str_ireplace('\\', '.', $namespace);
4344
$content[] = '';
4445
$content[] = " namespace {$namespace} {";
4546
foreach ($classes as $entry) {
@@ -63,11 +64,6 @@ public static function namespaceBlocksGenerate(array $namespaceGroups, array $cl
6364
public static function contentBlocksGenerate(array $props, string $name, string $stereotype)
6465
{
6566
$content = [" class {$name} {"];
66-
if (empty($props) && !$stereotype) {
67-
$content[] = '';
68-
69-
return $content;
70-
}
7167
if ($stereotype) {
7268
$content[] = " <<{$stereotype}>>";
7369
}
@@ -104,7 +100,7 @@ public static function addCompositionRelations(array $types, string $name, array
104100
if ($targetName === $name) {
105101
continue;
106102
}
107-
$cardinality = ($type['isNullable'] ?? false) ? '"0..1"' : '"1"';
103+
$cardinality = $type['allowsNull'] ? '"0..1"' : '"1"';
108104
$relations[] = " {$name} --> {$cardinality} {$targetName} : " . ($type['typename']['short'] ?? '');
109105
}
110106

src/ImmutableBase.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -735,8 +735,12 @@ private static function applyArrayDeepUpdate(array $current, array $subPaths, st
735735
{
736736
foreach ($subPaths as $path => $value) {
737737
if (\is_string($path)) {
738-
[$index, $rest] = explode($separator, $path, 2);
739-
$grouped[$index][$rest] = $value;
738+
$target = explode($separator, $path, 2);
739+
if (\count($target) === 2) {
740+
$grouped[$target[0]][$target[1]] = $value;
741+
} else {
742+
$grouped[$target[0]] = $value;
743+
}
740744
} else {
741745
$current[$path] = $value;
742746
}
@@ -785,11 +789,11 @@ private static function jsonLike(mixed $value): bool
785789
*/
786790
final public static function loadCache(): void
787791
{
788-
$path = StaticStatus::$cachePath ??= dirname(dirname((new ReflectionClass(ClassLoader::class))->getFileName()), 2) . '/ib-cache.php';
792+
$path = StaticStatus::$cachePath ??= dirname(dirname((new ReflectionClass(ClassLoader::class))->getFileName()), 2) . '/ib-cache.php'; // @codeCoverageIgnore
789793
if (!StaticStatus::$cachedMeta && file_exists($path)) {
790794
// This file is a machine-generated array from var_export (Cache).
791795
// Since it's data-as-code and paths are dynamic, PSR-4 'use' is not applicable.
792-
StaticStatus::$cachedMeta = require_once $path; // NOSONAR
796+
StaticStatus::$cachedMeta = require $path; // NOSONAR
793797
}
794798
}
795799

@@ -965,6 +969,9 @@ final public function with(string | array | object $data, string $separator = '.
965969
$current instanceof self => $current->with($sub, $separator),
966970
default => self::applyArrayDeepUpdate($current, $sub, $separator),
967971
};
972+
if ($types[$root]['arrayOf'] !== null) {
973+
$values[$root] = self::resolveValue($types[$root], $values[$root], false);
974+
}
968975
}
969976
$ref = StaticStatus::$refs[$static] ??= new ReflectionClass($static);
970977
$instance = $ref->newInstanceWithoutConstructor();

0 commit comments

Comments
 (0)