• 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

66.04
/src/Mapping/Core/ConfigurationBuilder.php
1
<?php
2

3
namespace Ninja\Granite\Mapping\Core;
4

5
use Ninja\Granite\Exceptions\ReflectionException;
6
use Ninja\Granite\Mapping\Contracts\MappingCache;
7
use Ninja\Granite\Mapping\Contracts\NamingConvention;
8
use Ninja\Granite\Mapping\ConventionMapper;
9
use Ninja\Granite\Mapping\Exceptions\MappingException;
10
use Ninja\Granite\Mapping\MappingProfile;
11
use Ninja\Granite\Mapping\PropertyMapping;
12
use Ninja\Granite\Mapping\Traits\MappingStorageTrait;
13
use Ninja\Granite\Mapping\TypeMapping;
14
use Ninja\Granite\Support\ReflectionCache;
15
use ReflectionClass;
16
use ReflectionProperty;
17

18
/**
19
 * Builds and manages mapping configurations.
20
 * Handles profile registration, convention mapping, and caching.
21
 */
22
final class ConfigurationBuilder
23
{
24
    use MappingStorageTrait;
25

26
    private MappingCache $cache;
27
    private ?ConventionMapper $conventionMapper;
28
    private array $profiles = [];
29
    private bool $useConventions;
30

31
    public function __construct(
10✔
32
        MappingCache $cache,
33
        bool $useConventions = false,
34
        float $conventionThreshold = 0.8,
35
    ) {
36
        $this->cache = $cache;
10✔
37
        $this->useConventions = $useConventions;
10✔
38
        $this->conventionMapper = $useConventions
10✔
39
            ? new ConventionMapper(null, $conventionThreshold)
×
40
            : null;
10✔
41
    }
42

43
    /**
44
     * Get mapping configuration for source to destination.
45
     */
46
    public function getConfiguration(mixed $source, string $destinationType): array
21✔
47
    {
48
        $sourceType = is_object($source) ? get_class($source) : 'array';
21✔
49

50
        // Check cache first
51
        if ($this->cache->has($sourceType, $destinationType)) {
21✔
52
            $cached = $this->cache->get($sourceType, $destinationType);
11✔
53
            return is_array($cached) ? $cached : [];
11✔
54
        }
55

56
        // Build new configuration
57
        $config = $this->buildConfiguration($sourceType, $destinationType);
12✔
58

59
        // Cache it
60
        $this->cache->put($sourceType, $destinationType, $config);
12✔
61

62
        return $config;
12✔
63
    }
64

65
    /**
66
     * Create reverse configuration for existing mapping.
67
     * @throws MappingException
68
     */
NEW
69
    public function createReverseConfiguration(string $sourceType, string $destinationType, TypeMapping $reverseMapping): void
×
70
    {
NEW
71
        $originalConfig = $this->getConfiguration($sourceType, $destinationType);
×
72

NEW
73
        foreach ($originalConfig as $destProp => $config) {
×
NEW
74
            if ( ! is_array($config)) {
×
NEW
75
                continue;
×
76
            }
77

NEW
78
            $sourceProp = $config['source'] ?? null;
×
79

80
            // Skip if no explicit source property or complex transformers
NEW
81
            if (null === $sourceProp || $sourceProp === $destProp || ($config['transformer'] ?? null) !== null) {
×
NEW
82
                continue;
×
83
            }
84

NEW
85
            if (is_string($sourceProp) && is_string($destProp)) {
×
86
                // Create reverse mapping
NEW
87
                $reverseMapping->forMember($sourceProp, fn($m) => $m->mapFrom($destProp));
×
88
            }
89
        }
90
    }
91

92
    // =================
93
    // Profile Management
94
    // =================
95

96
    public function addProfile(MappingProfile $profile): void
10✔
97
    {
98
        $this->profiles[] = $profile;
10✔
99
    }
100

101
    public function warmupCache(array $profiles): void
10✔
102
    {
103
        foreach ($profiles as $profile) {
10✔
104
            if ($profile instanceof MappingProfile) {
10✔
105
                $this->warmupProfileCache($profile);
10✔
106
            }
107
        }
108
    }
109

110
    // ===================
111
    // Convention Management
112
    // ===================
113

NEW
114
    public function enableConventions(bool $enabled): void
×
115
    {
NEW
116
        $this->useConventions = $enabled;
×
117

NEW
118
        if ($enabled && null === $this->conventionMapper) {
×
NEW
119
            $this->conventionMapper = new ConventionMapper();
×
120
        }
121
    }
122

NEW
123
    public function setConventionThreshold(float $threshold): void
×
124
    {
NEW
125
        $this->conventionMapper?->setConfidenceThreshold($threshold);
×
126
    }
127

NEW
128
    public function registerConvention(NamingConvention $convention): void
×
129
    {
NEW
130
        $this->conventionMapper?->registerConvention($convention);
×
131
    }
132

133
    // ==============
134
    // Cache Management
135
    // ==============
136

NEW
137
    public function clearCache(): void
×
138
    {
NEW
139
        $this->conventionMapper?->clearMappingsCache();
×
140
    }
141

142
    /**
143
     * Build mapping configuration from profiles and conventions.
144
     */
145
    private function buildConfiguration(string $sourceType, string $destinationType): array
21✔
146
    {
147
        $config = [];
21✔
148

149
        // Get destination properties
150
        $properties = $this->getDestinationProperties($destinationType);
21✔
151

152
        foreach ($properties as $property) {
21✔
153
            if ( ! ($property instanceof ReflectionProperty)) {
21✔
NEW
154
                continue;
×
155
            }
156

157
            $propertyName = $property->getName();
21✔
158

159
            // Check for explicit mapping from profiles
160
            $mapping = $this->findExplicitMapping($sourceType, $destinationType, $propertyName);
21✔
161

162
            if (null !== $mapping) {
21✔
163
                $config[$propertyName] = $this->buildPropertyConfig($mapping, $propertyName);
9✔
164
            } else {
165
                // Build from attributes or conventions
166
                $config[$propertyName] = $this->buildPropertyFromAttributes($property, $sourceType, $destinationType);
13✔
167
            }
168
        }
169

170
        // Apply convention-based mapping if enabled
171
        if ($this->useConventions && null !== $this->conventionMapper) {
21✔
172
            $config = $this->applyConventionMappings($sourceType, $destinationType, $config);
11✔
173
        }
174

175
        return $config;
21✔
176
    }
177

178
    /**
179
     * Find explicit mapping from registered profiles.
180
     */
181
    private function findExplicitMapping(string $sourceType, string $destinationType, string $property): ?PropertyMapping
21✔
182
    {
183
        // Check direct mappings first
184
        $mapping = $this->getMapping($sourceType, $destinationType, $property);
21✔
185
        if (null !== $mapping) {
21✔
186
            return $mapping;
×
187
        }
188

189
        // Check profiles
190
        foreach ($this->profiles as $profile) {
21✔
191
            $mapping = $profile->getMapping($sourceType, $destinationType, $property);
10✔
192
            if ($mapping instanceof PropertyMapping) {
10✔
193
                return $mapping;
9✔
194
            }
195
        }
196

197
        return null;
13✔
198
    }
199

200
    /**
201
     * Build property configuration from PropertyMapping.
202
     */
203
    private function buildPropertyConfig(PropertyMapping $mapping, string $propertyName): array
9✔
204
    {
205
        return [
9✔
206
            'source' => $mapping->getSourceProperty() ?? $propertyName,
9✔
207
            'transformer' => $mapping->getTransformer(),
9✔
208
            'condition' => $mapping->getCondition(),
9✔
209
            'default' => $mapping->getDefaultValue(),
9✔
210
            'hasDefault' => $mapping->hasDefaultValue(),
9✔
211
            'ignore' => $mapping->isIgnored(),
9✔
212
        ];
9✔
213
    }
214

215
    /**
216
     * Build property configuration from attributes.
217
     */
218
    private function buildPropertyFromAttributes(ReflectionProperty $property, string $sourceType, string $destinationType): array
13✔
219
    {
220
        $attributeProcessor = new AttributeProcessor();
13✔
221
        return $attributeProcessor->processProperty($property);
13✔
222
    }
223

224
    /**
225
     * Apply convention-based mappings.
226
     */
227
    private function applyConventionMappings(string $sourceType, string $destinationType, array $config): array
11✔
228
    {
229
        if ('array' === $sourceType || ! class_exists($sourceType) || ! class_exists($destinationType) || null === $this->conventionMapper) {
11✔
230
            return $config;
11✔
231
        }
232

233
        $conventionMappings = $this->conventionMapper->discoverMappings($sourceType, $destinationType);
×
234

235
        foreach ($conventionMappings as $destProperty => $sourceProperty) {
×
236
            // Only apply if no explicit mapping exists
NEW
237
            if ( ! isset($config[$destProperty]) || (
×
NEW
238
                is_array($config[$destProperty]) &&
×
NEW
239
                isset($config[$destProperty]['source']) &&
×
NEW
240
                $config[$destProperty]['source'] === $destProperty
×
241
            )) {
NEW
242
                if ( ! is_array($config[$destProperty])) {
×
NEW
243
                    $config[$destProperty] = [];
×
244
                }
245
                $config[$destProperty]['source'] = $sourceProperty;
×
246
            }
247
        }
248

249
        return $config;
×
250
    }
251

252
    /**
253
     * Get destination type properties.
254
     * @param string $destinationType
255
     * @throws ReflectionException
256
     */
257
    private function getDestinationProperties(string $destinationType): array
21✔
258
    {
259
        if ( ! class_exists($destinationType)) {
21✔
NEW
260
            return [];
×
261
        }
262

263
        return ReflectionCache::getPublicProperties($destinationType);
21✔
264
    }
265

266
    private function warmupProfileCache(MappingProfile $profile): void
10✔
267
    {
268
        // Extract mappings from profile and warm up cache
269
        $reflection = new ReflectionClass($profile);
10✔
270
        $mappingsProperty = $reflection->getProperty('mappings');
10✔
271
        $mappingsProperty->setAccessible(true);
10✔
272

273
        $mappings = $mappingsProperty->getValue($profile);
10✔
274

275
        if ( ! is_array($mappings)) {
10✔
NEW
276
            return;
×
277
        }
278

279
        foreach ($mappings as $key => $propertyMappings) {
10✔
280
            if ( ! is_string($key)) {
9✔
NEW
281
                continue;
×
282
            }
283
            [$sourceType, $destinationType] = explode('->', $key);
9✔
284

285
            if ( ! $this->cache->has($sourceType, $destinationType)) {
9✔
286
                $config = $this->buildConfiguration($sourceType, $destinationType);
9✔
287
                $this->cache->put($sourceType, $destinationType, $config);
9✔
288
            }
289
        }
290
    }
291
}
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