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

avoutic / web-framework / 19506833906

19 Nov 2025 03:30PM UTC coverage: 73.091% (-0.2%) from 73.336%
19506833906

push

github

avoutic
TaskRunner long options now accept `--option=value` as well as `--option value`

0 of 13 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1972 of 2698 relevant lines covered (73.09%)

2.78 hits per line

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

0.0
/src/Task/TaskRunner.php
1
<?php
2

3
/*
4
 * This file is part of WebFramework.
5
 *
6
 * (c) Avoutic <avoutic@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace WebFramework\Task;
13

14
use DI\Container;
15
use DI\ContainerBuilder;
16
use WebFramework\Config\ConfigBuilder;
17
use WebFramework\Core\BootstrapService;
18
use WebFramework\Core\EnvLoader;
19
use WebFramework\Exception\ArgumentParserException;
20

21
/**
22
 * Class TaskRunner.
23
 *
24
 * Manages the execution of tasks, including configuration and container setup.
25
 *
26
 * This class is not instantiated by the config or the Container, but initializes config and container for
27
 * the task to run.
28
 */
29
class TaskRunner
30
{
31
    private ConfigBuilder $configBuilder;
32

33
    /** @var ContainerBuilder<Container> */
34
    private ContainerBuilder $containerBuilder;
35
    private ?Container $container = null;
36
    private bool $isPlaintext = false;
37

38
    /** @var array<string> Default configuration files */
39
    private array $configFiles = [
40
        '/vendor/avoutic/web-framework/config/base_config.php',
41
        '/config/config.php',
42
        '?/config/config_local.php',
43
    ];
44

45
    /** @var ?array<string> */
46
    private ?array $definitionFiles = null;
47

48
    /**
49
     * TaskRunner constructor.
50
     *
51
     * @param string $appDir The application directory
52
     */
53
    public function __construct(
×
54
        private string $appDir,
55
    ) {
56
        $this->configBuilder = new ConfigBuilder($this->appDir);
×
57
        $this->containerBuilder = new ContainerBuilder();
×
58
    }
59

60
    /**
61
     * Set the configuration files to use.
62
     *
63
     * @param array<string> $configs An array of configuration file paths
64
     */
65
    public function setConfigFiles(array $configs): void
×
66
    {
67
        $this->configFiles = $configs;
×
68
    }
69

70
    /**
71
     * Set the definition files to use.
72
     *
73
     * @param array<string> $definitions An array of definition file paths
74
     */
75
    public function setDefinitionFiles(array $definitions): void
×
76
    {
77
        $this->definitionFiles = $definitions;
×
78
    }
79

80
    /**
81
     * Get a service from the container.
82
     *
83
     * @param string $key The service key
84
     *
85
     * @return mixed The requested service
86
     *
87
     * @throws \RuntimeException If the container is not initialized
88
     */
89
    public function get(string $key): mixed
×
90
    {
91
        if (!$this->container)
×
92
        {
93
            throw new \RuntimeException('Container not yet initialized');
×
94
        }
95

96
        return $this->container->get($key);
×
97
    }
98

99
    /**
100
     * Set the task to run in plaintext mode.
101
     */
102
    public function setPlaintext(): void
×
103
    {
104
        $this->isPlaintext = true;
×
105
    }
106

107
    public function loadEnv(): void
×
108
    {
109
        $envLoader = new EnvLoader();
×
110
        $envFile = "{$this->appDir}/.env";
×
111

112
        $appEnv = getenv('APP_ENV');
×
113
        if ($appEnv !== false)
×
114
        {
115
            if (file_exists("{$this->appDir}/.env.{$appEnv}"))
×
116
            {
117
                $envFile = "{$this->appDir}/.env.{$appEnv}";
×
118
            }
119
        }
120

121
        $envLoader->loadEnvFile($envFile);
×
122

123
        require_once __DIR__.'/../Environment.php';
×
124
    }
125

126
    /**
127
     * Build the configuration and container.
128
     */
129
    public function build(): void
×
130
    {
131
        $this->loadEnv();
×
132

133
        $config = $this->configBuilder->buildConfig(
×
134
            $this->configFiles,
×
135
        );
×
136

137
        // Build container
138
        //
139
        $this->containerBuilder->addDefinitions(['config_tree' => $this->configBuilder->getConfig()]);
×
140
        $this->containerBuilder->addDefinitions($this->configBuilder->getFlattenedConfig());
×
141
        $this->containerBuilder->addDefinitions(['is_plaintext' => $this->isPlaintext]);
×
142
        $this->containerBuilder->addDefinitions([self::class => $this]);
×
143

144
        $definitionFiles = $this->definitionFiles ?? $config['definition_files'];
×
145
        foreach ($definitionFiles as $file)
×
146
        {
147
            if ($file[0] == '?')
×
148
            {
149
                $file = substr($file, 1);
×
150

151
                if (!file_exists("{$this->appDir}/definitions/{$file}"))
×
152
                {
153
                    continue;
×
154
                }
155
            }
156

157
            $this->containerBuilder->addDefinitions("{$this->appDir}/definitions/{$file}");
×
158
        }
159

160
        $this->container = $this->containerBuilder->build();
×
161
    }
162

163
    /**
164
     * Apply arguments to a task.
165
     *
166
     * @param ConsoleTask   $task                 The task to apply arguments to
167
     * @param array<string> $commandLineArguments The arguments to apply
168
     *
169
     * @throws ArgumentParserException If the arguments are invalid
170
     */
171
    private function applyArguments(ConsoleTask $task, array $commandLineArguments): void
×
172
    {
173
        /** @var array<TaskOption> $options */
174
        $options = $task->getOptions();
×
175

176
        /** @var array<TaskArgument> $arguments */
177
        $arguments = $task->getArguments();
×
178
        $argumentIndex = 0;
×
179

180
        $i = 0;
×
181
        $argCount = count($commandLineArguments);
×
182
        while ($i < $argCount)
×
183
        {
184
            $arg = $commandLineArguments[$i];
×
185

186
            if (str_starts_with($arg, '--'))
×
187
            {
188
                $searchValue = substr($arg, 2);
×
NEW
189
                $inlineValue = null;
×
190

NEW
191
                $equalsPosition = strpos($searchValue, '=');
×
NEW
192
                if ($equalsPosition !== false)
×
193
                {
NEW
194
                    $inlineValue = substr($searchValue, $equalsPosition + 1);
×
NEW
195
                    $searchValue = substr($searchValue, 0, $equalsPosition);
×
196
                }
197

198
                $option = $this->findOptionByLong($options, $searchValue);
×
199

200
                if (!$option)
×
201
                {
202
                    throw new ArgumentParserException("Unknown option: {$arg}");
×
203
                }
204

205
                if ($option->hasValue())
×
206
                {
NEW
207
                    if ($inlineValue !== null)
×
208
                    {
NEW
209
                        $option->applyValue($inlineValue);
×
210
                    }
211
                    else
212
                    {
NEW
213
                        if ($i + 1 >= $argCount)
×
214
                        {
NEW
215
                            throw new ArgumentParserException("Option {$arg} requires a value");
×
216
                        }
217

NEW
218
                        $option->applyValue($commandLineArguments[$i + 1]);
×
NEW
219
                        $i++;
×
220
                    }
221
                }
222
                else
223
                {
NEW
224
                    if ($inlineValue !== null)
×
225
                    {
NEW
226
                        throw new ArgumentParserException("Option {$arg} does not accept a value");
×
227
                    }
228

UNCOV
229
                    $option->trigger();
×
230
                }
231
            }
232
            elseif (str_starts_with($arg, '-'))
×
233
            {
234
                $searchValue = substr($arg, 1);
×
235

236
                $option = $this->findOptionByShort($options, $searchValue);
×
237

238
                if (!$option)
×
239
                {
240
                    throw new ArgumentParserException("Unknown option: {$arg}");
×
241
                }
242

243
                if ($option->hasValue())
×
244
                {
245
                    if ($i + 1 >= $argCount)
×
246
                    {
247
                        throw new ArgumentParserException("Option {$arg} requires a value");
×
248
                    }
249

250
                    $option->applyValue($commandLineArguments[$i + 1]);
×
251
                    $i++;
×
252
                }
253
                else
254
                {
255
                    $option->trigger();
×
256
                }
257
            }
258
            else
259
            {
260
                if ($argumentIndex >= count($arguments))
×
261
                {
262
                    throw new ArgumentParserException('Too many arguments');
×
263
                }
264

265
                $argument = $arguments[$argumentIndex];
×
266
                $argument->apply($arg);
×
267
                $argumentIndex++;
×
268
            }
269

270
            $i++;
×
271
        }
272

273
        for ($remaining = $argumentIndex; $remaining < count($arguments); $remaining++)
×
274
        {
275
            if ($arguments[$remaining]->isRequired())
×
276
            {
277
                throw new ArgumentParserException('Missing arguments');
×
278
            }
279
        }
280
    }
281

282
    /**
283
     * Locate an option by its long name.
284
     *
285
     * @param array<TaskOption> $options
286
     */
287
    private function findOptionByLong(array $options, string $long): ?TaskOption
×
288
    {
289
        foreach ($options as $option)
×
290
        {
291
            if ($option->getLong() === $long)
×
292
            {
293
                return $option;
×
294
            }
295
        }
296

297
        return null;
×
298
    }
299

300
    /**
301
     * Locate an option by its short name.
302
     *
303
     * @param array<TaskOption> $options
304
     */
305
    private function findOptionByShort(array $options, string $short): ?TaskOption
×
306
    {
307
        foreach ($options as $option)
×
308
        {
309
            if ($option->getShort() === $short)
×
310
            {
311
                return $option;
×
312
            }
313
        }
314

315
        return null;
×
316
    }
317

318
    /**
319
     * Execute a task.
320
     *
321
     * @param string        $taskClass The fully qualified class name of the task to execute
322
     * @param array<string> $arguments The arguments to pass to the task
323
     *
324
     * @throws \RuntimeException       If the task does not implement Task
325
     * @throws ArgumentParserException If the arguments are invalid
326
     */
327
    public function execute(string $taskClass, array $arguments = []): void
×
328
    {
329
        $task = $this->get($taskClass);
×
330

331
        if (!$task instanceof Task)
×
332
        {
333
            throw new \RuntimeException("Task {$taskClass} does not implement Task");
×
334
        }
335

336
        $this->executeTaskObject($task, $arguments);
×
337
    }
338

339
    /**
340
     * Execute a task object.
341
     *
342
     * @param Task          $task      The task object to execute
343
     * @param array<string> $arguments The arguments to pass to the task
344
     *
345
     * @throws \RuntimeException       If the task does not implement Task
346
     * @throws ArgumentParserException If the arguments are invalid
347
     */
348
    public function executeTaskObject(Task $task, array $arguments = []): void
×
349
    {
350
        if ($task instanceof ConsoleTask)
×
351
        {
352
            $this->applyArguments($task, $arguments);
×
353
        }
354

355
        // Bootstrap the application if the task doesn't handle its own bootstrapping
356
        if (!$task->handlesOwnBootstrapping())
×
357
        {
358
            $bootstrapService = $this->get(BootstrapService::class);
×
359
            $bootstrapService->bootstrap();
×
360
        }
361

362
        $task->execute();
×
363
    }
364
}
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