Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.31% covered (danger)
31.31%
31 / 99
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractRenamespacerCommand
31.31% covered (danger)
31.31%
31 / 99
0.00% covered (danger)
0.00%
0 / 7
163.91
0.00% covered (danger)
0.00%
0 / 1
 configure
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
3
 initialize
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
2
 configureLogger
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getConsoleLogger
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
110
 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\DependenciesCollection;
9use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
10use BrianHenryIE\Strauss\Composer\ProjectComposerPackage;
11use BrianHenryIE\Strauss\Helpers\Flysystem\FileSystem;
12use BrianHenryIE\Strauss\Helpers\Flysystem\ReadOnlyFileSystemAdapter;
13use BrianHenryIE\Strauss\Helpers\Flysystem\SymlinkProtectFilesystemAdapter;
14use BrianHenryIE\Strauss\Helpers\Log\PadColonColumnsLogProcessor;
15use BrianHenryIE\Strauss\Helpers\Log\RelativeFilepathLogProcessor;
16use Composer\Util\Platform;
17use Elazar\Flystream\FilesystemRegistry;
18use League\Flysystem\Config;
19use League\Flysystem\PathPrefixer;
20use Monolog\Handler\PsrHandler;
21use Monolog\Logger;
22use Monolog\Processor\PsrLogMessageProcessor;
23use Psr\Log\LoggerInterface;
24use Psr\Log\LogLevel;
25use Psr\Log\NullLogger;
26use Symfony\Component\Console\Command\Command;
27use Symfony\Component\Console\Input\InputInterface;
28use Symfony\Component\Console\Input\InputOption;
29use Symfony\Component\Console\Logger\ConsoleLogger;
30use Symfony\Component\Console\Output\OutputInterface;
31
32abstract class AbstractRenamespacerCommand extends Command
33{
34    /**
35     * @var LoggerInterface&Logger
36     */
37    protected $logger;
38
39    /** No trailing slash */
40    protected string $workingDir;
41
42    /** @var FileSystem */
43    protected Filesystem $filesystem;
44    protected ProjectComposerPackage $projectComposerPackage;
45
46    protected StraussConfig $config;
47
48    protected DependenciesCollection $flatDependencyTree;
49
50    /**
51     * Set name and description, call parent class to add dry-run, verbosity options.
52     *
53     * @used-by \Symfony\Component\Console\Command\Command::__construct
54     * @override {@see \Symfony\Component\Console\Command\Command::configure()} empty method.
55     *
56     * @return void
57     */
58    protected function configure()
59    {
60        $this->addOption(
61            'dry-run',
62            null,
63            InputOption::VALUE_OPTIONAL,
64            'Do not actually make any changes',
65            false
66        );
67
68        $this->addOption(
69            'info',
70            null,
71            InputOption::VALUE_OPTIONAL,
72            'output level',
73            false
74        );
75
76        $this->addOption(
77            'debug',
78            null,
79            InputOption::VALUE_OPTIONAL,
80            'output level',
81            false
82        );
83
84        /**
85         * When run via. `strauss.phar`, classes such as `InstalledVersions` are prefixed, but when installed
86         * via Composer, the unprefixed version is used.
87         *
88         * @var string $installedSymfonyVersion
89         */
90        $installedSymfonyVersion = class_exists(\BrianHenryIE\Strauss\Composer\InstalledVersions::class)
91            ? \BrianHenryIE\Strauss\Composer\InstalledVersions::getVersion('symfony/console')
92            : \Composer\InstalledVersions::getVersion('symfony/console');
93
94        if (version_compare($installedSymfonyVersion, '7.2', '<')) {
95            $this->addOption(
96                'silent',
97                's',
98                InputOption::VALUE_OPTIONAL,
99                'output level',
100                false
101            );
102        }
103    }
104
105    /**
106     * Symfony hook that runs before execute(). Sets working directory, filesystem and logger.
107     */
108    protected function initialize(InputInterface $input, OutputInterface $output): void
109    {
110        $this->flatDependencyTree = new DependenciesCollection([]);
111
112        $logger = new Logger('logger');
113        $this->logger = $logger;
114
115        $workingDir      = Platform::getcwd();
116        $localFsLocation = FileSystem::getFsRoot($workingDir);
117
118        $pathNormalizer = Filesystem::makePathNormalizer($localFsLocation);
119
120        $pathPrefixer = new PathPrefixer(
121            $localFsLocation,
122            DIRECTORY_SEPARATOR
123        );
124
125        /**
126         * `league/flysystem` v2.x throws deprecation errors on newer PHP versions.
127         * `league/flysystem` v3.x requires PHP ^8.02 and Strauss's backward compatibility promise keeps us at 7.4 until WordPress itself requires newer PHP.
128         */
129        set_error_handler(function () {
130        }, E_DEPRECATED | E_USER_DEPRECATED);
131
132        // Extends `LocalFilesystemAdapter`.
133        $localFilesystemAdapter = new SymlinkProtectFilesystemAdapter(
134            $localFsLocation,
135            $pathNormalizer,
136            $pathPrefixer,
137            $logger
138        );
139
140        $this->filesystem = new FileSystem(
141            $localFilesystemAdapter,
142            [
143                    Config::OPTION_DIRECTORY_VISIBILITY => 'public',
144                ],
145            $pathNormalizer,
146            $pathPrefixer,
147            $localFsLocation,
148            $workingDir,
149        );
150
151        restore_error_handler();
152
153        $this->workingDir = $this->filesystem->normalizePath($workingDir);
154
155        $this->configureLogger($logger, $input, $output);
156    }
157
158    protected function configureLogger(Logger $logger, InputInterface $input, OutputInterface $output): void
159    {
160        $logger->pushProcessor(new PsrLogMessageProcessor());
161        $logger->pushProcessor(new RelativeFilepathLogProcessor($this->filesystem));
162        $logger->pushProcessor(new PadColonColumnsLogProcessor());
163        $logger->pushHandler(new PsrHandler($this->getConsoleLogger($input, $output)));
164    }
165
166    public function setLogger(LoggerInterface $logger): void
167    {
168        $this->logger->pushHandler(new PsrHandler($logger));
169    }
170
171    protected function execute(InputInterface $input, OutputInterface $output): int
172    {
173        if (!isset($this->config)) {
174            $this->config = $this->createConfig($input);
175        }
176
177        if ($this->config->isDryRun()) {
178            /**
179             * `league/flysystem` v2.x throws deprecation errors on newer PHP versions.
180             * `league/flysystem` v3.x requires PHP ^8.02 and Strauss's backward compatibility promise keeps us at 7.4 until WordPress itself requires newer PHP.
181             */
182            set_error_handler(function () {
183            }, E_DEPRECATED | E_USER_DEPRECATED);
184
185            $this->filesystem->setAdapter(
186                new ReadOnlyFileSystemAdapter(
187                    $this->filesystem->getAdapter(),
188                    Filesystem::makePathNormalizer($this->workingDir)
189                )
190            );
191            $this->filesystem->setLocalFsLocation('mem://');
192
193            restore_error_handler();
194
195            /** @var FilesystemRegistry $registry */
196            $registry = \Elazar\Flystream\ServiceLocator::get(\Elazar\Flystream\FilesystemRegistry::class);
197
198            // Register a file stream mem:// to handle file operations by third party libraries.
199            // This exception handling probably doesn't matter in real life but does in unit tests.
200            try {
201                $registry->get('mem');
202            } catch (\Exception $e) {
203                $registry->register('mem', $this->filesystem);
204            }
205
206            $this->logger->reset();
207            $this->configureLogger($this->logger, $input, $output);
208        }
209
210        return Command::SUCCESS;
211    }
212
213    /**
214     * Build a logger honoring optional --info/--debug/--silent flags if present.
215     */
216    protected function getConsoleLogger(InputInterface $input, OutputInterface $output): LoggerInterface
217    {
218        // If a subclass has a config and it is a dry-run, increase verbosity
219        $isDryRun = isset($this->config) && $this->config->isDryRun();
220
221        // Who would want to dry-run without output?
222        if (!$isDryRun && $input->hasOption('silent') && $input->getOption('silent') !== false) {
223            return new NullLogger();
224        }
225
226        $logLevel = [LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL];
227
228        if ($input->hasOption('info') && $input->getOption('info') !== false) {
229            $logLevel[LogLevel::INFO] = OutputInterface::VERBOSITY_NORMAL;
230        }
231
232        if ($isDryRun || ($input->hasOption('debug') && $input->getOption('debug') !== false)) {
233            $logLevel[LogLevel::INFO] = OutputInterface::VERBOSITY_NORMAL;
234            $logLevel[LogLevel::DEBUG] = OutputInterface::VERBOSITY_NORMAL;
235        }
236
237        return new ConsoleLogger($output, $logLevel);
238    }
239
240    protected function createConfig(InputInterface $input): StraussConfig
241    {
242        return new StraussConfig();
243    }
244}