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

diego-ninja / granite / 18554422350

16 Oct 2025 07:59AM UTC coverage: 82.689% (-0.4%) from 83.133%
18554422350

Pull #15

github

web-flow
Merge 8e39c5ac3 into dd1631bf7
Pull Request #15: refactor: hydrators

126 of 136 new or added lines in 9 files covered. (92.65%)

25 existing lines in 1 file now uncovered.

2651 of 3206 relevant lines covered (82.69%)

16.55 hits per line

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

87.5
/src/Hydration/HydratorFactory.php
1
<?php
2

3
namespace Ninja\Granite\Hydration;
4

5
use Ninja\Granite\Hydration\Contracts\Hydrator;
6
use Ninja\Granite\Hydration\Hydrators\ArrayHydrator;
7
use Ninja\Granite\Hydration\Hydrators\GetterHydrator;
8
use Ninja\Granite\Hydration\Hydrators\GraniteHydrator;
9
use Ninja\Granite\Hydration\Hydrators\JsonHydrator;
10
use Ninja\Granite\Hydration\Hydrators\ObjectHydrator;
11
use Ninja\Granite\Hydration\Hydrators\StringHydrator;
12
use RuntimeException;
13

14
/**
15
 * Factory for creating and managing hydrators.
16
 *
17
 * This factory maintains a registry of hydrators and resolves
18
 * the appropriate hydrator(s) for given data.
19
 */
20
class HydratorFactory
21
{
22
    private static ?self $instance = null;
23

24
    /** @var array<Hydrator> */
25
    private array $hydrators = [];
26

27
    /** @var bool */
28
    private bool $sorted = false;
29

30
    private function __construct()
1✔
31
    {
32
        $this->registerDefaultHydrators();
1✔
33
    }
34

35
    /**
36
     * Get the singleton instance.
37
     */
38
    public static function getInstance(): self
121✔
39
    {
40
        if (null === self::$instance) {
121✔
41
            self::$instance = new self();
1✔
42
        }
43

44
        return self::$instance;
121✔
45
    }
46

47
    /**
48
     * Reset the factory to its default state (useful for testing).
49
     */
NEW
50
    public static function reset(): void
×
51
    {
NEW
52
        self::$instance = null;
×
53
    }
54

55
    /**
56
     * Register a custom hydrator.
57
     *
58
     * @param Hydrator $hydrator Hydrator instance to register
59
     * @return self For method chaining
60
     */
61
    public function register(Hydrator $hydrator): self
1✔
62
    {
63
        $this->hydrators[] = $hydrator;
1✔
64
        $this->sorted = false;
1✔
65

66
        return $this;
1✔
67
    }
68

69
    /**
70
     * Get all registered hydrators sorted by priority.
71
     *
72
     * @return array<Hydrator>
73
     */
74
    public function getHydrators(): array
121✔
75
    {
76
        if ( ! $this->sorted) {
121✔
77
            usort($this->hydrators, fn($a, $b) => $b->getPriority() <=> $a->getPriority());
1✔
78
            $this->sorted = true;
1✔
79
        }
80

81
        return $this->hydrators;
121✔
82
    }
83

84
    /**
85
     * Resolve the appropriate hydrator for the given data.
86
     *
87
     * @param mixed $data Data to hydrate
88
     * @param string $targetClass Target class being hydrated
89
     * @return Hydrator|null The appropriate hydrator or null if none found
90
     */
91
    public function resolve(mixed $data, string $targetClass): ?Hydrator
95✔
92
    {
93
        foreach ($this->getHydrators() as $hydrator) {
95✔
94
            if ($hydrator->supports($data, $targetClass)) {
95✔
95
                return $hydrator;
95✔
96
            }
97
        }
98

NEW
99
        return null;
×
100
    }
101

102
    /**
103
     * Hydrate data using all applicable hydrators.
104
     *
105
     * This method uses a chain of hydrators to extract as much data as possible.
106
     * It's particularly useful for objects that may have both public properties
107
     * and getters.
108
     *
109
     * @param mixed $data Source data
110
     * @param string $targetClass Target class being hydrated
111
     * @return array Normalized data
112
     * @throws RuntimeException If no suitable hydrator is found
113
     */
114
    public function hydrateWith(mixed $data, string $targetClass): array
121✔
115
    {
116
        // For non-objects, use single hydrator
117
        if ( ! is_object($data)) {
121✔
118
            $hydrator = $this->resolve($data, $targetClass);
95✔
119

120
            if (null === $hydrator) {
95✔
NEW
121
                throw new RuntimeException(
×
NEW
122
                    sprintf('No hydrator found for data type: %s', get_debug_type($data)),
×
NEW
123
                );
×
124
            }
125

126
            return $hydrator->hydrate($data, $targetClass);
95✔
127
        }
128

129
        // For objects, use chain of hydrators to extract maximum data
130
        return $this->hydrateObjectWithChain($data, $targetClass);
35✔
131
    }
132

133
    /**
134
     * Register default hydrators.
135
     */
136
    private function registerDefaultHydrators(): void
1✔
137
    {
138
        $this->register(new GraniteHydrator());
1✔
139
        $this->register(new JsonHydrator());
1✔
140
        $this->register(new ArrayHydrator());
1✔
141
        $this->register(new ObjectHydrator());
1✔
142
        $this->register(new GetterHydrator());
1✔
143
        $this->register(new StringHydrator()); // Catch-all for invalid strings
1✔
144
    }
145

146
    /**
147
     * Hydrate an object using a chain of hydrators.
148
     *
149
     * This allows combining data from multiple extraction strategies:
150
     * 1. First, try high-priority hydrators (Granite, toArray, JsonSerializable, public props)
151
     * 2. Then, enrich with getter-based extraction
152
     *
153
     * @param object $data Source object
154
     * @param string $targetClass Target class
155
     * @return array Combined extracted data
156
     */
157
    private function hydrateObjectWithChain(object $data, string $targetClass): array
35✔
158
    {
159
        $extractedData = [];
35✔
160

161
        foreach ($this->getHydrators() as $hydrator) {
35✔
162
            if ( ! $hydrator->supports($data, $targetClass)) {
35✔
163
                continue;
35✔
164
            }
165

166
            // Special handling for GetterHydrator - pass existing data
167
            if ($hydrator instanceof GetterHydrator) {
35✔
168
                $additionalData = $hydrator->extractViaGetters($data, $extractedData, $targetClass);
35✔
169
                $extractedData = array_merge($extractedData, $additionalData);
35✔
170
            } else {
171
                // For other hydrators, use their result and stop chain
172
                $extractedData = $hydrator->hydrate($data, $targetClass);
35✔
173
                // Don't break - let GetterHydrator enrich the data
174
            }
175
        }
176

177
        return $extractedData;
35✔
178
    }
179
}
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