Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.71% covered (danger)
35.71%
30 / 84
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractRenamespacerCommand
35.71% covered (danger)
35.71%
30 / 84
20.00% covered (danger)
20.00%
1 / 5
150.58
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 initialize
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getLogger
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
156
 createConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Log level, filesystem
4 */
5
6namespace BrianHenryIE\Strauss\Console\Commands;
7
8use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
9use BrianHenryIE\Strauss\Composer\ProjectComposerPackage;
10use BrianHenryIE\Strauss\Helpers\FileSystem;
11use BrianHenryIE\Strauss\Helpers\Log\PadColonColumnsLogProcessor;
12use BrianHenryIE\Strauss\Helpers\Log\RelativeFilepathLogProcessor;
13use BrianHenryIE\Strauss\Helpers\ReadOnlyFileSystem;
14use Composer\InstalledVersions;
15use Elazar\Flystream\FilesystemRegistry;
16use Elazar\Flystream\StripProtocolPathNormalizer;
17use League\Flysystem\WhitespacePathNormalizer;
18use Monolog\Handler\PsrHandler;
19use Monolog\Logger;
20use Monolog\Processor\PsrLogMessageProcessor;
21use Psr\Log\LoggerAwareTrait;
22use Symfony\Component\Console\Command\Command;
23use Symfony\Component\Console\Input\InputInterface;
24use Symfony\Component\Console\Input\InputOption;
25use Symfony\Component\Console\Output\OutputInterface;
26use League\Flysystem\Config;
27use League\Flysystem\Local\LocalFilesystemAdapter;
28use Psr\Log\LoggerInterface;
29use Psr\Log\LogLevel;
30use Psr\Log\NullLogger;
31use Symfony\Component\Console\Logger\ConsoleLogger;
32
33abstract class AbstractRenamespacerCommand extends Command
34{
35    use LoggerAwareTrait;
36
37    /** @var string */
38    protected string $workingDir;
39
40    /** @var FileSystem */
41    protected Filesystem $filesystem;
42
43    protected ProjectComposerPackage $projectComposerPackage;
44
45    protected StraussConfig $config;
46
47    /**
48     * Set name and description, call parent class to add dry-run, verbosity options.
49     *
50     * @used-by \Symfony\Component\Console\Command\Command::__construct
51     * @override {@see \Symfony\Component\Console\Command\Command::configure()} empty method.
52     *
53     * @return void
54     */
55    protected function configure()
56    {
57        $this->addOption(
58            'dry-run',
59            null,
60            InputOption::VALUE_OPTIONAL,
61            'Do not actually make any changes',
62            false
63        );
64
65        $this->addOption(
66            'info',
67            null,
68            InputOption::VALUE_OPTIONAL,
69            'output level',
70            false
71        );
72
73        $this->addOption(
74            'debug',
75            null,
76            InputOption::VALUE_OPTIONAL,
77            'output level',
78            false
79        );
80
81        /** @var string $installedSymfonyVersion */
82        $installedSymfonyVersion = InstalledVersions::getVersion('symfony/console');
83
84        if (version_compare($installedSymfonyVersion, '7.2', '<')) {
85            $this->addOption(
86                'silent',
87                's',
88                InputOption::VALUE_OPTIONAL,
89                'output level',
90                false
91            );
92        }
93    }
94
95    protected function execute(InputInterface $input, OutputInterface $output): int
96    {
97        if (!isset($this->config)) {
98            $this->config = $this->createConfig($input);
99        }
100
101        if ($this->config->isDryRun()) {
102            $normalizer = new WhitespacePathNormalizer();
103            $normalizer = new StripProtocolPathNormalizer(['mem'], $normalizer);
104
105            $this->filesystem =
106                new FileSystem(
107                    new ReadOnlyFileSystem(
108                        $this->filesystem,
109                        $normalizer
110                    ),
111                    $this->workingDir
112                );
113
114            /** @var FilesystemRegistry $registry */
115            $registry = \Elazar\Flystream\ServiceLocator::get(\Elazar\Flystream\FilesystemRegistry::class);
116
117            // Register a file stream mem:// to handle file operations by third party libraries.
118            // This exception handling probably doesn't matter in real life but does in unit tests.
119            try {
120                $registry->get('mem');
121            } catch (\Exception $e) {
122                $registry->register('mem', $this->filesystem);
123            }
124        }
125
126        $logger = new Logger('logger');
127        $logger->pushProcessor(new PsrLogMessageProcessor());
128        $logger->pushProcessor(new RelativeFilepathLogProcessor($this->filesystem));
129        $logger->pushProcessor(new PadColonColumnsLogProcessor());
130        $logger->pushHandler(new PsrHandler($this->getLogger($input, $output)));
131        $this->setLogger($logger);
132
133        return Command::SUCCESS;
134    }
135
136    /**
137     * Symfony hook that runs before execute(). Sets working directory, filesystem and logger.
138     */
139    protected function initialize(InputInterface $input, OutputInterface $output): void
140    {
141        $this->workingDir = getcwd() . '/';
142
143        if (!isset($this->filesystem)) {
144            $localFilesystemAdapter = new LocalFilesystemAdapter(
145                '/',
146                null,
147                LOCK_EX,
148                LocalFilesystemAdapter::SKIP_LINKS
149            );
150
151            $this->filesystem = new FileSystem(
152                new \League\Flysystem\Filesystem(
153                    $localFilesystemAdapter,
154                    [
155                        Config::OPTION_DIRECTORY_VISIBILITY => 'public',
156                    ]
157                ),
158                $this->workingDir
159            );
160        }
161
162        if (method_exists($this, 'setLogger')) {
163            $this->setLogger($this->getLogger($input, $output));
164        }
165    }
166
167    /**
168     * Build a logger honoring optional --info/--debug/--silent flags if present.
169     */
170    protected function getLogger(InputInterface $input, OutputInterface $output): LoggerInterface
171    {
172        // If a subclass has a config and it is a dry-run, increase verbosity
173        $isDryRun = property_exists($this, 'config') && isset($this->config) && method_exists($this->config, 'isDryRun') && $this->config->isDryRun();
174
175        // Who would want to dry-run without output?
176        if (!$isDryRun && $input->hasOption('silent') && $input->getOption('silent') !== false) {
177            return new NullLogger();
178        }
179
180        $logLevel = [LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL];
181
182        if ($input->hasOption('info') && $input->getOption('info') !== false) {
183            $logLevel[LogLevel::INFO] = OutputInterface::VERBOSITY_NORMAL;
184        }
185
186        if ($isDryRun || ($input->hasOption('debug') && $input->getOption('debug') !== false)) {
187            $logLevel[LogLevel::INFO] = OutputInterface::VERBOSITY_NORMAL;
188            $logLevel[LogLevel::DEBUG] = OutputInterface::VERBOSITY_NORMAL;
189        }
190
191        return new ConsoleLogger($output, $logLevel);
192    }
193
194
195    protected function createConfig(InputInterface $input): StraussConfig
196    {
197        return new StraussConfig();
198    }
199}