• 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

10.32
/src/Mapping/TypeMapping.php
1
<?php
2

3
namespace Ninja\Granite\Mapping;
4

5
use Exception;
6
use Ninja\Granite\Exceptions\ReflectionException;
7
use Ninja\Granite\Mapping\Contracts\MappingStorage;
8
use Ninja\Granite\Mapping\Contracts\Transformer;
9
use Ninja\Granite\Mapping\Exceptions\MappingException;
10
use Ninja\Granite\Support\ReflectionCache;
11
use ReflectionProperty;
12

13
/**
14
 * Configures mapping between source and destination types.
15
 */
16
final class TypeMapping
17
{
18
    /**
19
     * Whether the mapping is sealed.
20
     */
21
    private bool $sealed = false;
22

23
    /**
24
     * Constructor.
25
     *
26
     * @param MappingStorage $storage Mapping storage
27
     * @param string $sourceType Source type name
28
     * @param string $destinationType Destination type name
29
     */
30
    public function __construct(
28✔
31
        private readonly MappingStorage $storage,
32
        private readonly string $sourceType,
33
        private readonly string $destinationType,
34
    ) {}
28✔
35

36
    /**
37
     * Configure mapping for a specific destination property.
38
     *
39
     * @param string $destinationProperty Destination property name
40
     * @param callable $configuration Configuration function
41
     * @return $this For method chaining
42
     * @throws MappingException If the mapping is already sealed
43
     */
44
    public function forMember(string $destinationProperty, callable $configuration): self
26✔
45
    {
46
        if ($this->sealed) {
26✔
47
            throw new MappingException(
×
48
                $this->sourceType,
×
49
                $this->destinationType,
×
50
                "Cannot modify mapping after it has been sealed",
×
NEW
51
                $destinationProperty,
×
52
            );
×
53
        }
54

55
        $mapping = new PropertyMapping();
26✔
56
        $configuration($mapping);
26✔
57

58
        $this->storage->addPropertyMapping(
26✔
59
            $this->sourceType,
26✔
60
            $this->destinationType,
26✔
61
            $destinationProperty,
26✔
62
            $mapping,
26✔
63
        );
26✔
64

65
        return $this;
26✔
66
    }
67

68
    /**
69
     * Validate all mappings and finalize the configuration.
70
     *
71
     * @return $this For method chaining
72
     * @throws MappingException If validation fails
73
     */
74
    public function seal(): self
×
75
    {
76
        if ($this->sealed) {
×
77
            return $this; // Already sealed
×
78
        }
79

80
        try {
81
            // Get all configured mappings for this type pair
82
            $mappings = $this->storage->getMappingsForTypes($this->sourceType, $this->destinationType);
×
83

84
            // Validate destination properties exist
85
            $this->validateDestinationProperties($mappings);
×
86

87
            // Validate source properties when possible
NEW
88
            if ('array' !== $this->sourceType && class_exists($this->sourceType)) {
×
89
                $this->validateSourceProperties($mappings);
×
90
            }
91

92
            // Validate transformers and conditions
93
            $this->validateTransformersAndConditions($mappings);
×
94

95
            // Check for redundant or conflicting mappings
96
            $this->detectConflicts($mappings);
×
97

98
            // Mark as sealed
99
            $this->sealed = true;
×
100

101
            return $this;
×
102
        } catch (MappingException $e) {
×
103
            // Re-throw mapping exceptions
104
            throw $e;
×
NEW
105
        } catch (Exception $e) {
×
106
            // Wrap other exceptions
107
            throw new MappingException(
×
108
                $this->sourceType,
×
109
                $this->destinationType,
×
110
                "Error while validating mapping: " . $e->getMessage(),
×
111
                null,
×
112
                0,
×
NEW
113
                $e,
×
114
            );
×
115
        }
116
    }
117

118
    /**
119
     * Check if this mapping is sealed.
120
     *
121
     * @return bool Whether the mapping is sealed
122
     */
NEW
123
    public function isSealed(): bool
×
124
    {
NEW
125
        return $this->sealed;
×
126
    }
127

128
    /**
129
     * Get the source type.
130
     *
131
     * @return string Source type name
132
     */
NEW
133
    public function getSourceType(): string
×
134
    {
NEW
135
        return $this->sourceType;
×
136
    }
137

138
    /**
139
     * Get the destination type.
140
     *
141
     * @return string Destination type name
142
     */
NEW
143
    public function getDestinationType(): string
×
144
    {
NEW
145
        return $this->destinationType;
×
146
    }
147

148
    /**
149
     * Validate that all destination properties exist.
150
     *
151
     * @param array $mappings Property mappings to validate
152
     * @throws MappingException If a destination property doesn't exist
153
     */
154
    private function validateDestinationProperties(array $mappings): void
×
155
    {
NEW
156
        if ( ! class_exists($this->destinationType)) {
×
157
            throw new MappingException(
×
158
                $this->sourceType,
×
159
                $this->destinationType,
×
NEW
160
                "Destination type '{$this->destinationType}' does not exist",
×
161
            );
×
162
        }
163

164
        try {
165
            $destProperties = ReflectionCache::getPublicProperties($this->destinationType);
×
166
            $destPropNames = array_map(
×
167
                fn(ReflectionProperty $p) => $p->getName(),
×
NEW
168
                $destProperties,
×
169
            );
×
170

171
            foreach (array_keys($mappings) as $propName) {
×
NEW
172
                if ( ! in_array($propName, $destPropNames)) {
×
173
                    throw new MappingException(
×
174
                        $this->sourceType,
×
175
                        $this->destinationType,
×
176
                        "Destination property '{$propName}' does not exist in '{$this->destinationType}'",
×
NEW
177
                        $propName,
×
178
                    );
×
179
                }
180
            }
181
        } catch (ReflectionException $e) {
×
182
            throw new MappingException(
×
183
                $this->sourceType,
×
184
                $this->destinationType,
×
NEW
185
                "Error examining destination type: " . $e->getMessage(),
×
186
            );
×
187
        }
188
    }
189

190
    /**
191
     * Validate that all source properties exist.
192
     *
193
     * @param array $mappings Property mappings to validate
194
     * @throws MappingException If a source property doesn't exist
195
     */
196
    private function validateSourceProperties(array $mappings): void
×
197
    {
NEW
198
        if ( ! class_exists($this->sourceType)) {
×
NEW
199
            return; // Skip validation for non-class types like 'array'
×
200
        }
201

202
        try {
203
            $sourceProperties = ReflectionCache::getPublicProperties($this->sourceType);
×
204
            $sourcePropNames = array_map(
×
205
                fn(ReflectionProperty $p) => $p->getName(),
×
NEW
206
                $sourceProperties,
×
207
            );
×
208

209
            foreach ($mappings as $destProp => $mapping) {
×
210
                $sourceProp = $mapping->getSourceProperty();
×
211

212
                // Skip if no explicit source property or using dot notation (nested properties)
NEW
213
                if (null === $sourceProp || ! is_string($sourceProp) || str_contains($sourceProp, '.')) {
×
214
                    continue;
×
215
                }
216

NEW
217
                if ( ! in_array($sourceProp, $sourcePropNames)) {
×
218
                    throw new MappingException(
×
219
                        $this->sourceType,
×
220
                        $this->destinationType,
×
221
                        "Source property '{$sourceProp}' does not exist in '{$this->sourceType}'",
×
NEW
222
                        $destProp,
×
223
                    );
×
224
                }
225
            }
226
        } catch (ReflectionException $e) {
×
227
            throw new MappingException(
×
228
                $this->sourceType,
×
229
                $this->destinationType,
×
NEW
230
                "Error examining source type: " . $e->getMessage(),
×
231
            );
×
232
        }
233
    }
234

235
    /**
236
     * Validate transformers and conditions.
237
     *
238
     * @param array $mappings Property mappings to validate
239
     * @throws MappingException If a transformer or condition is invalid
240
     */
241
    private function validateTransformersAndConditions(array $mappings): void
×
242
    {
243
        foreach ($mappings as $destProp => $mapping) {
×
244
            $transformer = $mapping->getTransformer();
×
245

NEW
246
            if (null !== $transformer && ! ($transformer instanceof Transformer) && ! is_callable($transformer)) {
×
247
                throw new MappingException(
×
248
                    $this->sourceType,
×
249
                    $this->destinationType,
×
250
                    "Invalid transformer for property '{$destProp}': must be callable or implement Transformer",
×
NEW
251
                    $destProp,
×
252
                );
×
253
            }
254

255
            // We would also validate conditions here if PropertyMapping exposed the condition
256
            // For now we're assuming the condition is properly checked within PropertyMapping
257
        }
258
    }
259

260
    /**
261
     * Detect redundant or conflicting mappings.
262
     *
263
     * @param array $mappings Property mappings to check
264
     * @throws MappingException If conflicts are found
265
     */
266
    private function detectConflicts(array $mappings): void
×
267
    {
268
        $sourceProps = [];
×
269
        $ignoredProps = [];
×
270

271
        // Collect source properties and ignored flags
272
        foreach ($mappings as $destProp => $mapping) {
×
273
            if ($mapping->isIgnored()) {
×
274
                $ignoredProps[$destProp] = true;
×
275
                continue;
×
276
            }
277

278
            $sourceProp = $mapping->getSourceProperty();
×
NEW
279
            if (null !== $sourceProp) {
×
280
                $sourceProps[$sourceProp][] = $destProp;
×
281
            }
282
        }
283

284
        // Check for properties that are both mapped and ignored
285
        foreach ($ignoredProps as $destProp => $ignored) {
×
NEW
286
            if (isset($mappings[$destProp]) && null !== $mappings[$destProp]->getSourceProperty()) {
×
287
                throw new MappingException(
×
288
                    $this->sourceType,
×
289
                    $this->destinationType,
×
290
                    "Property '{$destProp}' is both mapped and ignored",
×
NEW
291
                    $destProp,
×
292
                );
×
293
            }
294
        }
295
    }
296
}
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