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

valksor / php-dev-build / 21323318062

24 Jan 2026 11:21PM UTC coverage: 27.706% (-2.8%) from 30.503%
21323318062

push

github

k0d3r1s
wip

1 of 2 new or added lines in 2 files covered. (50.0%)

909 existing lines in 16 files now uncovered.

791 of 2855 relevant lines covered (27.71%)

0.96 hits per line

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

91.3
/Provider/ProviderRegistry.php
1
<?php declare(strict_types = 1);
2

3
/*
4
 * This file is part of the Valksor package.
5
 *
6
 * (c) Davis Zalitis (k0d3r1s)
7
 * (c) SIA Valksor <packages@valksor.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace ValksorDev\Build\Provider;
14

15
use RuntimeException;
16
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
17

18
use function array_keys;
19
use function sprintf;
20
use function uasort;
21

22
/**
23
 * Registry for dev service providers with dependency resolution.
24
 *
25
 * This class serves as the central service provider registry for the Valksor build system.
26
 * It handles:
27
 * - Automatic service provider discovery via Symfony's autowiring iterator
28
 * - Service provider lookup by name with validation
29
 * - Configuration-based provider filtering by service flags (init, dev, prod)
30
 * - Dependency resolution and service ordering
31
 * - Provider validation against configuration
32
 *
33
 * The registry enables extensible service architecture where new build services
34
 * can be added by implementing ProviderInterface without modifying core build logic.
35
 */
36
final class ProviderRegistry
37
{
38
    /**
39
     * Registry of all available service providers indexed by service name.
40
     *
41
     * This array stores all discovered service providers where the key is the
42
     * provider's service name (e.g., 'tailwind', 'importmap', 'hot_reload')
43
     * and the value is the ProviderInterface implementation.
44
     *
45
     * @var array<string, ProviderInterface>
46
     */
47
    private array $providers = [];
48

49
    /**
50
     * Initialize the provider registry with auto-discovered service providers.
51
     *
52
     * This constructor uses Symfony's dependency injection container to automatically
53
     * discover all services tagged with 'valksor.service_provider'. The autowiring
54
     * iterator provides all registered providers without requiring manual registration.
55
     *
56
     * The autowiring mechanism allows new service providers to be added to the system
57
     * simply by tagging them in the service configuration, making the build system
58
     * highly extensible without code modifications.
59
     *
60
     * @param iterable<ProviderInterface> $providers Auto-discovered collection of all
61
     *                                               services tagged with 'valksor.service_provider'
62
     */
63
    public function __construct(
64
        #[AutowireIterator(
65
            'valksor.service_provider',
66
        )]
67
        iterable $providers,
68
    ) {
69
        // Register each discovered provider using the service name as the key
70
        foreach ($providers as $provider) {
75✔
71
            $this->register($provider);
4✔
72
        }
73
    }
74

75
    /**
76
     * Get a provider by service name.
77
     *
78
     * @param string $name The service name (e.g., 'tailwind')
79
     *
80
     * @return ProviderInterface The registered provider
81
     *
82
     * @throws RuntimeException If no provider is registered for this name
83
     */
84
    public function get(
85
        string $name,
86
    ): ProviderInterface {
87
        if (!$this->has($name)) {
9✔
88
            throw new RuntimeException(sprintf('No service provider registered for: %s', $name));
1✔
89
        }
90

91
        return $this->providers[$name];
8✔
92
    }
93

94
    /**
95
     * Get all registered provider names.
96
     *
97
     * @return array<int, string>
98
     */
99
    public function getAvailableNames(): array
100
    {
101
        return array_keys($this->providers);
2✔
102
    }
103

104
    /**
105
     * Get providers filtered by service configuration flags with dependency resolution.
106
     *
107
     * This method filters available service providers based on their configuration
108
     * flags (init, dev, prod) and resolves dependencies to ensure proper execution order.
109
     * It handles:
110
     * - Service enablement checking
111
     * - Flag-based filtering (init/dev/prod modes)
112
     * - Custom provider name resolution
113
     * - Dependency-aware sorting
114
     *
115
     * Example configuration structure:
116
     * [
117
     *     'tailwind' => [
118
     *         'enabled' => true,
119
     *         'flags' => ['dev' => true, 'prod' => true],
120
     *         'provider' => 'tailwind' // optional, defaults to service name
121
     *     ]
122
     * ]
123
     *
124
     * @param array  $servicesConfig Services configuration from valksor.php config file
125
     * @param string $flag           The flag to filter by ('init', 'dev', or 'prod')
126
     *
127
     * @return array<string, ProviderInterface> Filtered and sorted providers [service_name => provider]
128
     */
129
    public function getProvidersByFlag(
130
        array $servicesConfig,
131
        string $flag,
132
    ): array {
133
        $filteredProviders = [];
15✔
134

135
        // Process each service configuration entry
136
        foreach ($servicesConfig as $name => $config) {
15✔
137
            // Skip disabled services - allows conditional service enabling
138
            if (!($config['enabled'] ?? false)) {
13✔
139
                continue;
2✔
140
            }
141

142
            // Skip services that don't have the requested flag enabled
143
            // This enables mode-specific service execution (e.g., dev-only services)
144
            if (!($config['flags'][$flag] ?? false)) {
12✔
145
                continue;
8✔
146
            }
147

148
            // Resolve the actual provider name (supports custom provider mappings)
149
            // This allows one provider to serve multiple service configurations
150
            $providerName = $config['provider'] ?? $name;
6✔
151

152
            // Only include providers that actually exist in the registry
153
            if ($this->has($providerName)) {
6✔
154
                $filteredProviders[$name] = $this->get($providerName);
5✔
155
            }
156
        }
157

158
        // Sort providers by execution order and resolve dependencies
159
        return $this->sortProvidersByOrder($filteredProviders);
15✔
160
    }
161

162
    /**
163
     * Check if a provider is registered for the given service name.
164
     *
165
     * @param string $name The service name (e.g., 'tailwind')
166
     *
167
     * @return bool True if provider exists
168
     */
169
    public function has(
170
        string $name,
171
    ): bool {
172
        return isset($this->providers[$name]);
13✔
173
    }
174

175
    /**
176
     * Register a dev service provider.
177
     *
178
     * @param ProviderInterface $provider The provider to register
179
     */
180
    public function register(
181
        ProviderInterface $provider,
182
    ): void {
183
        $this->providers[$provider->getName()] = $provider;
14✔
184
    }
185

186
    /**
187
     * Validate that all configured providers exist.
188
     *
189
     * @param array $servicesConfig Services configuration from valksor.php
190
     *
191
     * @return array<string> Array of missing provider names
192
     */
193
    public function validateProviders(
194
        array $servicesConfig,
195
    ): array {
196
        $missing = [];
3✔
197

198
        foreach ($servicesConfig as $name => $config) {
3✔
199
            if (!($config['enabled'] ?? false)) {
3✔
200
                continue;
1✔
201
            }
202

203
            $providerName = $config['provider'] ?? $name;
3✔
204

205
            if (!$this->has($providerName)) {
3✔
206
                $missing[] = $providerName;
2✔
207
            }
208
        }
209

210
        return $missing;
3✔
211
    }
212

213
    /**
214
     * Sort providers by service order and resolve dependencies using topological sorting.
215
     *
216
     * This method implements a two-phase sorting algorithm:
217
     * 1. Primary sorting by configured service order (numerical priority)
218
     * 2. Dependency resolution to ensure dependent services run after their dependencies
219
     *
220
     * The dependency resolution uses a topological sort approach:
221
     * - Iteratively adds providers whose dependencies are already satisfied
222
     * - Detects circular dependencies and missing dependencies
223
     * - Falls back to original order for problematic configurations
224
     *
225
     * This ensures that services like 'hot_reload' (which depends on CSS/JS outputs)
226
     * run after 'tailwind' and 'importmap' have completed their builds.
227
     *
228
     * @param array<string, ProviderInterface> $providers Providers to sort and resolve
229
     *
230
     * @return array<string, ProviderInterface> Sorted providers with dependencies resolved
231
     */
232
    private function sortProvidersByOrder(
233
        array $providers,
234
    ): array {
235
        // Phase 1: Sort by service order (numerical priority)
236
        // Lower numbers typically indicate services that should run first
237
        uasort($providers, static fn (ProviderInterface $a, ProviderInterface $b) => $a->getServiceOrder() - $b->getServiceOrder());
15✔
238

239
        // Phase 2: Topological sort for dependency resolution
240
        $sorted = [];
15✔
241
        $remaining = $providers;
15✔
242

243
        // Continue until all providers are sorted or no progress can be made
244
        while (!empty($remaining)) {
15✔
245
            $addedInIteration = false;
5✔
246

247
            // Find providers whose dependencies are already satisfied
248
            foreach ($remaining as $name => $provider) {
5✔
249
                $dependencies = $provider->getDependencies();
5✔
250
                $allDependenciesMet = true;
5✔
251

252
                // Check if all declared dependencies are already in the sorted list
253
                foreach ($dependencies as $dependency) {
5✔
254
                    if (!isset($sorted[$dependency])) {
1✔
255
                        $allDependenciesMet = false;
×
256

UNCOV
257
                        break; // Missing dependency - this provider must wait
×
258
                    }
259
                }
260

261
                // If all dependencies are satisfied, add this provider to the sorted list
262
                if ($allDependenciesMet) {
5✔
263
                    $sorted[$name] = $provider;
5✔
264
                    unset($remaining[$name]);
5✔
265
                    $addedInIteration = true;
5✔
266
                }
267
            }
268

269
            // Circular dependency detection: if no providers were added in this iteration
270
            // but some remain, we have either circular dependencies or missing dependencies
271
            if (!$addedInIteration) {
5✔
272
                // Add remaining providers in their current order to prevent infinite loops
273
                // This allows the system to continue running even with misconfigured dependencies
274
                $sorted += $remaining;
×
275

UNCOV
276
                break;
×
277
            }
278
        }
279

280
        return $sorted;
15✔
281
    }
282
}
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