• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

diego-ninja / granite / 16608963009

29 Jul 2025 10:37PM UTC coverage: 50.423%. First build
16608963009

Pull #5

github

web-flow
Merge 43d8840d7 into 6a6caca51
Pull Request #5: code: adds phpstan level max, pint with per and github actions

321 of 632 new or added lines in 77 files covered. (50.79%)

1132 of 2245 relevant lines covered (50.42%)

9.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.3
/src/Mapping/ConventionMapper.php
1
<?php
2

3
namespace Ninja\Granite\Mapping;
4

5
use Ninja\Granite\Mapping\Contracts\NamingConvention;
6
use Ninja\Granite\Mapping\Conventions\ConventionRegistry;
7
use ReflectionClass;
8
use ReflectionException;
9
use ReflectionProperty;
10

11
class ConventionMapper
12
{
13
    /**
14
     * @var ConventionRegistry
15
     */
16
    private ConventionRegistry $registry;
17

18
    /**
19
     * @var float Confidence threshold for considering a match
20
     */
21
    private float $confidenceThreshold;
22

23
    /**
24
     * @var array Cache of discovered mappings
25
     */
26
    private array $discoveredMappings = [];
27

28
    public function __construct(
12✔
29
        ?ConventionRegistry $registry = null,
30
        float $confidenceThreshold = 0.8,
31
    ) {
32
        $this->registry = $registry ?? new ConventionRegistry();
12✔
33
        $this->confidenceThreshold = $confidenceThreshold;
12✔
34
    }
35

36
    /**
37
     * Detects the naming convention for a type.
38
     *
39
     * @param class-string $typeName Class name
40
     * @return NamingConvention|null Detected convention or null if it cannot be determined
41
     * @throws ReflectionException
42
     */
43
    public function detectConvention(string $typeName): ?NamingConvention
1✔
44
    {
45
        $reflection = new ReflectionClass($typeName);
1✔
46
        $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
1✔
47

48
        $conventions = $this->registry->getAll();
1✔
49
        $scores = array_fill_keys(array_keys($conventions), 0);
1✔
50

51
        foreach ($properties as $property) {
1✔
52
            $name = $property->getName();
1✔
53

54
            foreach ($conventions as $key => $convention) {
1✔
55
                if ($convention->matches($name)) {
1✔
56
                    $scores[$key]++;
1✔
57
                }
58
            }
59
        }
60

61
        // Determine the predominant convention
62
        arsort($scores);
1✔
63
        $topConvention = key($scores);
1✔
64

65
        if ( ! is_string($topConvention) || ! isset($conventions[$topConvention]) || $scores[$topConvention] <= 0) {
1✔
NEW
66
            return null;
×
67
        }
68

69
        return $conventions[$topConvention];
1✔
70
    }
71

72
    /**
73
     * Finds the best match between two properties based on conventions.
74
     *
75
     * @param string $sourceName Source property name
76
     * @param string $destinationName Destination property name
77
     * @return float Match confidence (0.0-1.0)
78
     */
79
    public function calculateConfidence(string $sourceName, string $destinationName): float
9✔
80
    {
81
        $sourceConventions = $this->registry->getAll();
9✔
82
        $destinationConventions = $this->registry->getAll();
9✔
83

84
        $highestConfidence = 0.0;
9✔
85

86
        foreach ($sourceConventions as $sourceConvention) {
9✔
87
            if ( ! $sourceConvention->matches($sourceName)) {
9✔
88
                continue;
9✔
89
            }
90

91
            $normalized = $sourceConvention->normalize($sourceName);
9✔
92

93
            foreach ($destinationConventions as $destinationConvention) {
9✔
94
                if ( ! $destinationConvention->matches($destinationName)) {
9✔
95
                    continue;
9✔
96
                }
97

98
                $destinationNormalized = $destinationConvention->normalize($destinationName);
9✔
99

100
                // Calculate similarity between normalized forms
101
                $similarity = $this->calculateStringSimilarity($normalized, $destinationNormalized);
9✔
102

103
                if ($similarity > $highestConfidence) {
9✔
104
                    $highestConfidence = $similarity;
9✔
105
                }
106
            }
107
        }
108

109
        return $highestConfidence;
9✔
110
    }
111

112
    /**
113
     * Discovers automatic mappings between two types.
114
     *
115
     * @param class-string $sourceType Source type
116
     * @param class-string $destinationType Destination type
117
     * @return array Discovered mappings [destinationProperty => sourceProperty]
118
     */
119
    public function discoverMappings(string $sourceType, string $destinationType): array
9✔
120
    {
121
        $cacheKey = $sourceType . '->' . $destinationType;
9✔
122

123
        if (isset($this->discoveredMappings[$cacheKey])) {
9✔
124
            $cached = $this->discoveredMappings[$cacheKey];
2✔
125
            return is_array($cached) ? $cached : [];
2✔
126
        }
127

128
        // If source is not a class (e.g., 'array'), we can't use reflection on it
129
        if ( ! class_exists($sourceType)) {
9✔
130
            // Return empty mappings since we can't determine properties
131
            $this->discoveredMappings[$cacheKey] = [];
2✔
132
            return [];
2✔
133
        }
134

135
        $sourceReflection = new ReflectionClass($sourceType);
7✔
136
        $destinationReflection = new ReflectionClass($destinationType);
7✔
137

138
        $sourceProperties = $sourceReflection->getProperties(ReflectionProperty::IS_PUBLIC);
7✔
139
        $destinationProperties = $destinationReflection->getProperties(ReflectionProperty::IS_PUBLIC);
7✔
140

141
        $sourceNames = array_map(fn($prop) => $prop->getName(), $sourceProperties);
7✔
142
        $destinationNames = array_map(fn($prop) => $prop->getName(), $destinationProperties);
7✔
143

144
        $mappings = [];
7✔
145

146
        foreach ($destinationNames as $destinationName) {
7✔
147
            $bestMatch = null;
7✔
148
            $bestConfidence = 0.0;
7✔
149

150
            // Evaluar todas las propiedades fuente para encontrar la mejor coincidencia
151
            foreach ($sourceNames as $sourceName) {
7✔
152
                $confidence = $this->calculateConfidence($sourceName, $destinationName);
7✔
153

154
                // Actualizar el mejor coincidente solo si la confianza es mayor que la anterior
155
                if ($confidence > $bestConfidence) {
7✔
156
                    $bestMatch = $sourceName;
7✔
157
                    $bestConfidence = $confidence;
7✔
158
                }
159
            }
160

161
            // Solo incluir en el mapeo si la mejor coincidencia supera el umbral
162
            if (null !== $bestMatch && $bestConfidence >= $this->confidenceThreshold) {
7✔
163
                $mappings[$destinationName] = $bestMatch;
7✔
164
            }
165
        }
166

167
        $this->discoveredMappings[$cacheKey] = $mappings;
7✔
168

169
        return $mappings;
7✔
170
    }
171

172
    /**
173
     * Applies the discovered mappings to a TypeMapping.
174
     *
175
     * @param class-string $sourceType Source type
176
     * @param class-string $destinationType Destination type
177
     * @param TypeMapping|null $typeMapping Existing type mapping (optional)
178
     * @return array The discovered mappings
179
     */
180
    public function applyConventions(string $sourceType, string $destinationType, ?TypeMapping $typeMapping = null): array
4✔
181
    {
182
        // Skip if not a valid class
183
        if ( ! class_exists($sourceType)) {
4✔
184
            return [];
2✔
185
        }
186

187
        $mappings = $this->discoverMappings($sourceType, $destinationType);
2✔
188

189
        if (null !== $typeMapping) {
2✔
190
            foreach ($mappings as $destinationName => $sourceName) {
2✔
191
                if ($destinationName !== $sourceName) {
2✔
192
                    $typeMapping->forMember($destinationName, fn($mapping) => $mapping->mapFrom($sourceName));
2✔
193
                }
194
            }
195
        }
196

197
        return $mappings;
2✔
198
    }
199

200
    /**
201
     * Sets the confidence threshold for matches.
202
     *
203
     * @param float $threshold Threshold between 0.0 and 1.0
204
     * @return $this For method chaining
205
     */
206
    public function setConfidenceThreshold(float $threshold): self
2✔
207
    {
208
        $this->confidenceThreshold = max(0.0, min(1.0, $threshold));
2✔
209
        return $this;
2✔
210
    }
211

212
    /**
213
     * Registers a new convention.
214
     *
215
     * @param NamingConvention $convention Convention to register
216
     * @return $this For method chaining
217
     */
218
    public function registerConvention(NamingConvention $convention): self
×
219
    {
220
        $this->registry->register($convention);
×
221
        return $this;
×
222
    }
223

224
    /**
225
     * Clears the cache of discovered mappings.
226
     *
227
     * @return void
228
     */
229
    public function clearMappingsCache(): void
×
230
    {
231
        $this->discoveredMappings = [];
×
232
    }
233

234
    /**
235
     * Calculates similarity between two strings.
236
     * Uses a combination of Levenshtein distance and sound similarity.
237
     */
238
    private function calculateStringSimilarity(string $str1, string $str2): float
9✔
239
    {
240
        // Exactly equal
241
        if ($str1 === $str2) {
9✔
242
            return 1.0;
8✔
243
        }
244

245
        // Normalize to lowercase for comparison
246
        $str1 = mb_strtolower($str1);
8✔
247
        $str2 = mb_strtolower($str2);
8✔
248

249
        // If equal after normalization
250
        if ($str1 === $str2) {
8✔
NEW
251
            return 0.95;
×
252
        }
253

254
        // Calculate similarity based on Levenshtein distance
255
        $maxLength = max(mb_strlen($str1), mb_strlen($str2));
8✔
256
        if (0 === $maxLength) {
8✔
NEW
257
            return 0.0;
×
258
        }
259

260
        $levenshtein = levenshtein($str1, $str2);
8✔
261
        $levenshteinSimilarity = 1.0 - ($levenshtein / $maxLength);
8✔
262

263
        // Calculate sound similarity (Soundex)
264
        $soundexSimilarity = soundex($str1) === soundex($str2) ? 0.7 : 0.0;
8✔
265

266
        // Combine similarities
267
        return max($levenshteinSimilarity, $soundexSimilarity);
8✔
268
    }
269
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc