• 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

74.49
/src/Traits/HasComparison.php
1
<?php
2

3
namespace Ninja\Granite\Traits;
4

5
use BackedEnum;
6
use DateTimeInterface;
7
use Ninja\Granite\Exceptions\ComparisonException;
8
use Ninja\Granite\Exceptions\ReflectionException;
9
use Ninja\Granite\Exceptions\SerializationException;
10
use Ninja\Granite\Granite;
11
use Ninja\Granite\Support\ReflectionCache;
12
use UnitEnum;
13

14
trait HasComparison
15
{
16
    /**
17
     * Check if this DTO is equal to another DTO of the same type.
18
     *
19
     * Only compares initialized public properties.
20
     *
21
     * @param Granite $other Another Granite object to compare against
22
     * @return bool True if all properties are equal
23
     * @throws ReflectionException
24
     */
25
    public function equals(Granite $other): bool
16✔
26
    {
27
        // Early return if not same class
28
        if ( ! $other instanceof static) {
16✔
29
            return false;
1✔
30
        }
31

32
        $profile = ReflectionCache::getClassProfile(static::class);
15✔
33
        if ($profile->canUseFastPath) {
15✔
34
            return $profile->areEqual($this, $other);
4✔
35
        }
36

37
        $properties = ReflectionCache::getPublicProperties(static::class);
11✔
38

39
        foreach ($properties as $property) {
11✔
40
            // Skip uninitialized properties
41
            if ( ! $property->isInitialized($this) || ! $property->isInitialized($other)) {
11✔
42
                continue;
9✔
43
            }
44

45
            $currentValue = $property->getValue($this);
11✔
46
            $otherValue = $property->getValue($other);
11✔
47

48
            if ( ! $this->valuesAreEqual($currentValue, $otherValue)) {
11✔
49
                return false;
6✔
50
            }
51
        }
52

53
        return true;
5✔
54
    }
55

56
    /**
57
     * Get differences between this object and another.
58
     *
59
     * Returns an array of properties that differ, with their current and new values.
60
     * Nested Granite objects are recursively compared.
61
     *
62
     * @param Granite $other Another Granite object to compare against
63
     * @return array<string, mixed> Array of differences
64
     * @throws ComparisonException If comparison fails for incomparable types
65
     * @throws ReflectionException
66
     * @throws SerializationException
67
     */
68
    public function differs(Granite $other): array
8✔
69
    {
70
        if ( ! $other instanceof static) {
8✔
71
            throw ComparisonException::typeMismatch(static::class, $other::class);
1✔
72
        }
73

74
        $differences = [];
7✔
75
        $properties = ReflectionCache::getPublicProperties(static::class);
7✔
76

77
        foreach ($properties as $property) {
7✔
78
            $propertyName = $property->getName();
7✔
79

80
            // Skip uninitialized properties
81
            if ( ! $property->isInitialized($this) || ! $property->isInitialized($other)) {
7✔
82
                continue;
5✔
83
            }
84

85
            $currentValue = $property->getValue($this);
7✔
86
            $otherValue = $property->getValue($other);
7✔
87

88
            if ( ! $this->valuesAreEqual($currentValue, $otherValue)) {
7✔
89
                // Handle nested Granite objects
90
                if ($currentValue instanceof Granite && $otherValue instanceof Granite) {
6✔
91
                    try {
92
                        $nestedDifferences = $currentValue->differs($otherValue);
1✔
93
                        if ( ! empty($nestedDifferences)) {
1✔
94
                            $differences[$propertyName] = $nestedDifferences;
1✔
95
                        }
96
                    } catch (ComparisonException $e) {
×
97
                        // If nested comparison fails, treat as different values
98
                        $differences[$propertyName] = [
×
99
                            'current' => $this->valueToComparable($currentValue),
×
100
                            'new' => $this->valueToComparable($otherValue),
×
101
                        ];
×
102
                    }
103
                } else {
104
                    $differences[$propertyName] = [
6✔
105
                        'current' => $this->valueToComparable($currentValue),
6✔
106
                        'new' => $this->valueToComparable($otherValue),
6✔
107
                    ];
6✔
108
                }
109
            }
110
        }
111

112
        return $differences;
7✔
113
    }
114

115
    /**
116
     * Check if two values are equal.
117
     *
118
     * Handles null, scalars, arrays, objects, enums, DateTimes, and Granite objects.
119
     *
120
     * @param mixed $value1 First value
121
     * @param mixed $value2 Second value
122
     * @return bool True if values are equal
123
     * @throws ReflectionException
124
     */
125
    private function valuesAreEqual(mixed $value1, mixed $value2): bool
17✔
126
    {
127
        // Handle null cases
128
        if (null === $value1 && null === $value2) {
17✔
UNCOV
129
            return true;
×
130
        }
131

132
        if (null === $value1 || null === $value2) {
17✔
133
            return false;
1✔
134
        }
135

136
        // Handle Granite objects recursively
137
        if ($value1 instanceof Granite && $value2 instanceof Granite) {
17✔
138
            return $value1->equals($value2);
3✔
139
        }
140

141
        // Handle DateTimeInterface
142
        if ($value1 instanceof DateTimeInterface && $value2 instanceof DateTimeInterface) {
17✔
143
            return $value1->getTimestamp() === $value2->getTimestamp()
4✔
144
                && $value1->getTimezone()->getName() === $value2->getTimezone()->getName();
4✔
145
        }
146

147
        // Handle arrays recursively
148
        if (is_array($value1) && is_array($value2)) {
17✔
149
            return $this->arraysAreEqual($value1, $value2);
4✔
150
        }
151

152
        // Handle enums
153
        if ($value1 instanceof UnitEnum && $value2 instanceof UnitEnum) {
17✔
154
            if ($value1 instanceof BackedEnum && $value2 instanceof BackedEnum) {
3✔
155
                return $value1->value === $value2->value;
3✔
156
            }
157
            return $value1->name === $value2->name;
×
158
        }
159

160
        // Handle scalar values (string, int, float, bool)
161
        if (is_scalar($value1) && is_scalar($value2)) {
17✔
162
            return $value1 === $value2;
17✔
163
        }
164

165
        // Handle other objects - try __toString or compare class + serialization
166
        if (is_object($value1) && is_object($value2)) {
×
167
            // Different classes are always different
168
            if ($value1::class !== $value2::class) {
×
169
                return false;
×
170
            }
171

172
            // If both have __toString, compare string representation
173
            if (method_exists($value1, '__toString') && method_exists($value2, '__toString')) {
×
174
                return (string) $value1 === (string) $value2;
×
175
            }
176

177
            // Last resort: serialize and compare (slower but accurate)
178
            return serialize($value1) === serialize($value2);
×
179
        }
180

181
        // Different types are never equal
182
        return false;
×
183
    }
184

185
    /**
186
     * Deep comparison of arrays.
187
     *
188
     * @param array $array1 First array
189
     * @param array $array2 Second array
190
     * @return bool True if arrays are equal
191
     * @throws ReflectionException
192
     */
193
    private function arraysAreEqual(array $array1, array $array2): bool
4✔
194
    {
195
        // Different sizes mean different arrays
196
        if (count($array1) !== count($array2)) {
4✔
197
            return false;
1✔
198
        }
199

200
        // Compare each key-value pair
201
        foreach ($array1 as $key => $value) {
3✔
202
            if ( ! array_key_exists($key, $array2)) {
3✔
203
                return false;
×
204
            }
205

206
            if ( ! $this->valuesAreEqual($value, $array2[$key])) {
3✔
207
                return false;
2✔
208
            }
209
        }
210

211
        return true;
1✔
212
    }
213

214
    /**
215
     * Convert a value to a comparable representation for diff output.
216
     *
217
     * @param mixed $value Value to convert
218
     * @return mixed Comparable representation
219
     * @throws ReflectionException
220
     * @throws SerializationException
221
     */
222
    private function valueToComparable(mixed $value): mixed
6✔
223
    {
224
        if (null === $value) {
6✔
225
            return null;
1✔
226
        }
227

228
        if (is_scalar($value)) {
6✔
229
            return $value;
3✔
230
        }
231

232
        if ($value instanceof DateTimeInterface) {
4✔
233
            return $value->format('Y-m-d H:i:s.u P'); // Include microseconds and timezone
1✔
234
        }
235

236
        if ($value instanceof UnitEnum) {
3✔
237
            return $value instanceof BackedEnum ? $value->value : $value->name;
1✔
238
        }
239

240
        if ($value instanceof Granite) {
2✔
241
            return $value->array();
1✔
242
        }
243

244
        if (is_array($value)) {
1✔
245
            return array_map(fn($v) => $this->valueToComparable($v), $value);
1✔
246
        }
247

248
        // For other objects, try to get a meaningful representation
249
        if (is_object($value)) {
×
250
            if (method_exists($value, '__toString')) {
×
251
                return (string) $value;
×
252
            }
253

254
            if (method_exists($value, 'toArray')) {
×
255
                return $value->toArray();
×
256
            }
257

258
            // Last resort: class name + property dump
259
            return [
×
260
                '__class' => $value::class,
×
261
                '__string' => get_debug_type($value),
×
262
            ];
×
263
        }
264

265
        return get_debug_type($value);
×
266
    }
267
}
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