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

diego-ninja / granite / 22631927117

03 Mar 2026 04:13PM UTC coverage: 83.371% (+0.3%) from 83.117%
22631927117

push

github

web-flow
Merge pull request #20 from diego-ninja/feature/performance-optimizations

Performance optimizations & README reorganization

172 of 191 new or added lines in 7 files covered. (90.05%)

2 existing lines in 2 files now uncovered.

2973 of 3566 relevant lines covered (83.37%)

16.23 hits per line

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

84.62
/src/Serialization/MetadataCache.php
1
<?php
2

3
namespace Ninja\Granite\Serialization;
4

5
use Exception;
6
use Ninja\Granite\Mapping\Contracts\NamingConvention;
7
use Ninja\Granite\Mapping\Conventions\CamelCaseConvention;
8
use Ninja\Granite\Serialization\Attributes\Hidden;
9
use Ninja\Granite\Serialization\Attributes\SerializationConvention;
10
use Ninja\Granite\Serialization\Attributes\SerializedName;
11
use Ninja\Granite\Support\ReflectionCache;
12
use ReflectionAttribute;
13
use ReflectionException;
14
use ReflectionMethod;
15

16
class MetadataCache
17
{
18
    /**
19
     * Cache of serialization metadata by class name.
20
     *
21
     * @var array<string, Metadata>
22
     */
23
    private static array $metadataCache = [];
24

25
    /**
26
     * Get serialization metadata for a class.
27
     *
28
     * @param class-string $class Class name
29
     * @return Metadata Serialization metadata
30
     * @throws \Ninja\Granite\Exceptions\ReflectionException
31
     */
32
    public static function getMetadata(string $class): Metadata
122✔
33
    {
34
        if ( ! isset(self::$metadataCache[$class])) {
122✔
35
            self::$metadataCache[$class] = self::buildMetadata($class);
54✔
36
        }
37

38
        return self::$metadataCache[$class];
121✔
39
    }
40

41
    /**
42
     * Check if a data key could represent the given PHP property via the convention.
43
     *
44
     * @param string $dataKey Key from input data
45
     * @param string $phpPropertyName PHP property name
46
     * @param NamingConvention $convention The naming convention to use for comparison
47
     * @return bool Whether they represent the same logical property
48
     */
49
    public static function conventionMatches(string $dataKey, string $phpPropertyName, NamingConvention $convention): bool
5✔
50
    {
51
        try {
52
            // Strategy 1: Normalize the data key using the target convention
53
            $dataKeyNormalized = null;
5✔
54
            if ($convention->matches($dataKey)) {
5✔
55
                $dataKeyNormalized = $convention->normalize($dataKey);
5✔
56
            }
57

58
            // Strategy 2: Normalize the PHP property name (assume camelCase source)
59
            $sourceConvention = new CamelCaseConvention();
5✔
60
            $phpPropertyNormalized = null;
5✔
61

62
            if ($sourceConvention->matches($phpPropertyName)) {
5✔
63
                $phpPropertyNormalized = $sourceConvention->normalize($phpPropertyName);
5✔
64
            } else {
65
                // Fallback normalization for PHP property names
66
                $converted = preg_replace('/(?<!^)([A-Z])/', ' $1', $phpPropertyName);
×
67
                $phpPropertyNormalized = mb_strtolower($converted ?? $phpPropertyName);
×
68
            }
69

70
            // Strategy 3: Compare normalized forms
71
            if (null !== $dataKeyNormalized) {
5✔
72
                return $dataKeyNormalized === $phpPropertyNormalized;
5✔
73
            }
74

75
            // Strategy 4: Try direct conversion - convert PHP property to target convention and compare
76
            $phpPropertyInTargetConvention = self::convertPropertyNameToConvention($phpPropertyName, $convention);
2✔
77
            return (bool) ($phpPropertyInTargetConvention === $dataKey)
2✔
78

79

80

81
            ;
2✔
82

83
        } catch (Exception $e) {
×
84
            return false;
×
85
        }
86
    }
87

88
    /**
89
     * Clear the metadata cache.
90
     * Useful for testing or when class definitions change dynamically.
91
     */
92
    public static function clearCache(): void
16✔
93
    {
94
        self::$metadataCache = [];
16✔
95
    }
96

97
    /**
98
     * Build serialization metadata for a class.
99
     *
100
     * @param class-string $class Class name
101
     * @return Metadata Built metadata
102
     * @throws \Ninja\Granite\Exceptions\ReflectionException
103
     */
104
    private static function buildMetadata(string $class): Metadata
54✔
105
    {
106
        $metadata = new Metadata();
54✔
107
        $reflection = ReflectionCache::getClass($class);
54✔
108

109
        // Check for class-level SerializationConvention attribute
110
        $classConvention = self::getClassConvention($class);
53✔
111

112
        // Check if the class has serializedNames() method and call it using reflection
113
        if (method_exists($class, 'serializedNames')) {
53✔
114
            $propertyNames = self::invokeProtectedStaticMethod($class, 'serializedNames');
48✔
115
            if (is_array($propertyNames)) {
48✔
116
                foreach ($propertyNames as $propName => $serializedName) {
48✔
117
                    if (is_string($propName) && is_string($serializedName)) {
5✔
118
                        $metadata->mapPropertyName($propName, $serializedName);
5✔
119
                    }
120
                }
121
            }
122
        }
123

124
        // Check if the class has hiddenProperties() method and call it using reflection
125
        if (method_exists($class, 'hiddenProperties')) {
53✔
126
            $hiddenProps = self::invokeProtectedStaticMethod($class, 'hiddenProperties');
48✔
127
            if (is_array($hiddenProps)) {
48✔
128
                foreach ($hiddenProps as $propName) {
48✔
129
                    if (is_string($propName)) {
5✔
130
                        $metadata->hideProperty($propName);
5✔
131
                    }
132
                }
133
            }
134
        }
135

136
        // Check for property attributes (PHP 8+)
137
        $properties = ReflectionCache::getPublicProperties($class);
53✔
138

139
        foreach ($properties as $property) {
53✔
140
            $propertyName = $property->getName();
52✔
141

142
            // Check for SerializedName attribute (takes precedence over convention)
143
            $nameAttrs = $property->getAttributes(SerializedName::class, ReflectionAttribute::IS_INSTANCEOF);
52✔
144
            if ( ! empty($nameAttrs)) {
52✔
145
                $attr = $nameAttrs[0]->newInstance();
8✔
146
                $metadata->mapPropertyName($propertyName, $attr->name);
8✔
147
            }
148
            // Apply class convention if no explicit SerializedName
149
            elseif (null !== $classConvention) {
52✔
150
                $conventionName = self::convertPropertyNameToConvention($propertyName, $classConvention);
15✔
151
                if ($conventionName !== $propertyName) {
15✔
152
                    $metadata->mapPropertyName($propertyName, $conventionName);
15✔
153
                }
154
            }
155

156
            // Check for Hidden attribute
157
            $hiddenAttrs = $property->getAttributes(Hidden::class, ReflectionAttribute::IS_INSTANCEOF);
52✔
158
            if ( ! empty($hiddenAttrs)) {
52✔
159
                $metadata->hideProperty($propertyName);
9✔
160
            }
161
        }
162

163
        return $metadata;
53✔
164
    }
165

166
    /**
167
     * Get the class-level naming convention if defined.
168
     *
169
     * @param class-string $class Class name
170
     * @return NamingConvention|null The naming convention or null if not defined
171
     */
172
    private static function getClassConvention(string $class): ?NamingConvention
53✔
173
    {
174
        try {
175
            $reflection = ReflectionCache::getClass($class);
53✔
176
            $conventionAttrs = $reflection->getAttributes(SerializationConvention::class, ReflectionAttribute::IS_INSTANCEOF);
53✔
177

178
            if (empty($conventionAttrs)) {
53✔
179
                return null;
38✔
180
            }
181

182
            $conventionAttr = $conventionAttrs[0]->newInstance();
15✔
183
            return $conventionAttr->getConvention();
15✔
184
        } catch (Exception $e) {
×
185
            // Log error and return null to fallback gracefully
186
            return null;
×
187
        }
188
    }
189

190
    /**
191
     * Convert a PHP property name (assumed camelCase) to the target convention.
192
     *
193
     * @param string $propertyName The PHP property name (typically camelCase)
194
     * @param NamingConvention $targetConvention The target naming convention
195
     * @return string The converted property name
196
     */
197
    private static function convertPropertyNameToConvention(string $propertyName, NamingConvention $targetConvention): string
15✔
198
    {
199
        try {
200
            // Step 1: Detect the source convention (assume camelCase for PHP properties)
201
            $sourceConvention = new CamelCaseConvention();
15✔
202

203
            // Step 2: Normalize the property name to a standard form
204
            $normalized = $propertyName;
15✔
205

206
            // If the property name matches camelCase convention, normalize it
207
            if ($sourceConvention->matches($propertyName)) {
15✔
208
                $normalized = $sourceConvention->normalize($propertyName);
15✔
209
            } else {
210
                // Fallback: try to convert camelCase-like strings to normalized form
211
                // Example: "firstName" -> "first name", "XMLHttpRequest" -> "xml http request"
212
                $converted = preg_replace('/(?<!^)([A-Z])/', ' $1', $propertyName);
×
213
                $normalized = mb_strtolower($converted ?? $propertyName);
×
214
            }
215

216
            // Step 3: Convert normalized form to target convention
217
            return $targetConvention->denormalize($normalized);
15✔
218

219
        } catch (Exception $e) {
×
220
            // Return original name if conversion fails
221
            return $propertyName;
×
222
        }
223
    }
224

225
    /**
226
     * Invoke a protected static method using reflection.
227
     *
228
     * @param string $class Class name
229
     * @param string $methodName Method name
230
     * @return mixed Method result
231
     * @throws \Ninja\Granite\Exceptions\ReflectionException
232
     */
233
    private static function invokeProtectedStaticMethod(string $class, string $methodName): mixed
48✔
234
    {
235
        try {
236
            $reflectionMethod = new ReflectionMethod($class, $methodName);
48✔
237
            return $reflectionMethod->invoke(null);
48✔
UNCOV
238
        } catch (ReflectionException $e) {
×
239
            throw new \Ninja\Granite\Exceptions\ReflectionException($class, $methodName, $e->getMessage());
×
240
        }
241
    }
242
}
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