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

sanmai / console / 15898353940

26 Jun 2025 09:36AM UTC coverage: 96.429% (-3.6%) from 100.0%
15898353940

Pull #5

github

web-flow
Merge 4318e11eb into 6babeaf10
Pull Request #5: Skip commands with required arguments

8 of 9 new or added lines in 1 file covered. (88.89%)

1 existing line in 1 file now uncovered.

27 of 28 relevant lines covered (96.43%)

11.0 hits per line

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

95.65
/src/ClassmapCommandProvider.php
1
<?php
2

3
/**
4
 * Copyright 2025 Alexey Kopytko <alexey@kopytko.com>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
declare(strict_types=1);
20

21
namespace ConsoleApp;
22

23
use Composer\Autoload\ClassLoader;
24
use IteratorAggregate;
25
use Later\Interfaces\Deferred;
26
use Symfony\Component\Console\Command\Command;
27
use Traversable;
28
use Override;
29
use ReflectionClass;
30

31
use function is_subclass_of;
32
use function Later\later;
33
use function Pipeline\take;
34
use function str_contains;
35
use function str_ends_with;
36

37
/**
38
 * Command provider that discovers commands from Composer's classmap
39
 *
40
 * @implements IteratorAggregate<Command>
41
 * @final
42
 */
43
class ClassmapCommandProvider implements IteratorAggregate
44
{
45
    /** @var Deferred<Traversable<Command>> */
46
    private readonly Deferred $commands;
47

48
    public function __construct(private readonly ClassLoader $classLoader)
49
    {
50
        $this->commands = later(fn() => yield $this->listCommands());
16✔
51
    }
52

53
    /**
54
     * @return Traversable<Command>
55
     */
56
    private function listCommands(): Traversable
57
    {
58
        return take($this->classLoader->getClassMap())
16✔
59
            ->stream()
16✔
60
            ->cast(realpath(...))
16✔
61
            ->filter(self::isNotVendoredDependency(...))
16✔
62
            ->filter(self::hasCommandInFilename(...))
16✔
63
            ->keys()
16✔
64
            ->filter(self::isCommandSubclass(...))
16✔
65
            ->filter(self::canBeInstantiatedWithoutArguments(...))
16✔
66
            ->cast(self::newCommand(...));
16✔
67
    }
68

69
    private static function isNotVendoredDependency(string $filename): bool
70
    {
71
        return !str_contains($filename, '/vendor/');
12✔
72
    }
73

74
    private static function hasCommandInFilename(string $filename): bool
75
    {
76
        return str_ends_with($filename, 'Command.php');
12✔
77
    }
78

79
    /**
80
     * @param class-string $class
81
     */
82
    private static function isCommandSubclass(string $class): bool
83
    {
84
        return is_subclass_of($class, Command::class);
8✔
85
    }
86

87
    /**
88
     * @param class-string<Command> $class
89
     */
90
    private static function canBeInstantiatedWithoutArguments(string $class): bool
91
    {
92
        $reflection = new ReflectionClass($class);
8✔
93
        $constructor = $reflection->getConstructor();
8✔
94

95
        if (null === $constructor) {
8✔
NEW
UNCOV
96
            return true;
×
97
        }
98

99
        foreach ($constructor->getParameters() as $parameter) {
8✔
100
            if (!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()) {
8✔
101
                return false;
4✔
102
            }
103
        }
104

105
        return true;
4✔
106
    }
107

108
    /**
109
     * @param class-string<Command> $class
110
     */
111
    private static function newCommand(string $class): Command
112
    {
113
        /** @psalm-suppress UnsafeInstantiation */
114
        return new $class();
4✔
115
    }
116

117
    #[Override]
118
    public function getIterator(): Traversable
119
    {
120
        return $this->commands->get();
16✔
121
    }
122

123
}
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