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