UpdateCommand::compareMaps()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 3
dl 0
loc 20
rs 9.7
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace FileEye\MimeMap\Command;
4
5
use SebastianBergmann\Comparator\ComparisonFailure;
6
use SebastianBergmann\Comparator\Factory;
7
use SebastianBergmann\Diff\Differ;
8
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
9
use Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Output\OutputInterface;
13
use Symfony\Component\Console\Style\SymfonyStyle;
14
use Symfony\Component\Yaml\Yaml;
15
use FileEye\MimeMap\Map\MimeMapInterface;
16
use FileEye\MimeMap\MapHandler;
17
use FileEye\MimeMap\MapUpdater;
18
19
/**
20
 * A Symfony application command to update the MIME type to extension map.
21
 */
22
class UpdateCommand extends Command
23
{
24
    /**
25
     * {@inheritdoc}
26
     */
27
    protected function configure(): void
28
    {
29
        $this
30
            ->setName('update')
31
            ->setDescription('Updates the MIME-type-to-extension map. Executes the commands in the script file specified by --script, then writes the map to the PHP file where the PHP --class is defined.')
32
            ->addOption(
33
                'script',
34
                null,
35
                InputOption::VALUE_REQUIRED,
36
                'File name of the script containing the sequence of commands to execute to build the default map.',
37
                MapUpdater::getDefaultMapBuildFile()
38
            )
39
            ->addOption(
40
                'class',
41
                null,
42
                InputOption::VALUE_REQUIRED,
43
                'The fully qualified class name of the PHP class storing the map.',
44
                MapHandler::DEFAULT_MAP_CLASS
45
            )
46
            ->addOption(
47
                'diff',
48
                null,
49
                InputOption::VALUE_NONE,
50
                'Report updates.'
51
            )
52
            ->addOption(
53
                'fail-on-diff',
54
                null,
55
                InputOption::VALUE_NONE,
56
                'Exit with an error when a difference is found. Map will not be updated.'
57
            )
58
        ;
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    protected function execute(InputInterface $input, OutputInterface $output): int
65
    {
66
        $io = new SymfonyStyle($input, $output);
67
68
        $updater = new MapUpdater();
69
        $updater->selectBaseMap(MapUpdater::DEFAULT_BASE_MAP_CLASS);
70
71
        $scriptFile = $input->getOption('script');
72
        if (!is_string($scriptFile)) {
73
            $io->error('Invalid value for --script option.');
74
            return (2);
75
        }
76
77
        $mapClass = $input->getOption('class');
78
        if (!is_string($mapClass)) {
79
            $io->error('Invalid value for --class option.');
80
            return (2);
81
        }
82
83
        $diff = $input->getOption('diff');
84
        assert(is_bool($diff));
85
        $failOnDiff = $input->getOption('fail-on-diff');
86
        assert(is_bool($failOnDiff));
87
88
        // Executes on the base map the script commands.
89
        $contents = file_get_contents($scriptFile);
90
        if ($contents === false) {
91
            $io->error('Failed loading update script file ' . $scriptFile);
92
            return (2);
93
        }
94
95
        $commands = Yaml::parse($contents);
96
        if (!is_array($commands)) {
97
            $io->error('Invalid update script file ' . $scriptFile);
98
            return (2);
99
        }
100
101
        foreach ($commands as $command) {
102
            $output->writeln("<info>{$command[0]} ...</info>");
103
            try {
104
                $callable = [$updater, $command[1]];
105
                assert(is_callable($callable));
106
                $errors = call_user_func_array($callable, $command[2]);
107
                assert(is_array($errors));
108
                if (!empty($errors)) {
109
                    foreach ($errors as $error) {
110
                        $output->writeln("<comment>$error.</comment>");
111
                    }
112
                }
113
            } catch (\Exception $e) {
114
                $io->error($e->getMessage());
115
                return(1);
116
            }
117
        }
118
119
        // Load the map to be changed.
120
        MapHandler::setDefaultMapClass($mapClass);
121
        $current_map = MapHandler::map();
122
123
        // Check if anything got changed.
124
        $write = true;
125
        if ($diff) {
126
            $write = false;
127
            foreach ([
128
                't' => 'MIME types',
129
                'a' => 'MIME type aliases',
130
                'e' => 'extensions',
131
            ] as $key => $desc) {
132
                try {
133
                    $output->writeln("<info>Checking changes to {$desc} ...</info>");
134
                    $this->compareMaps($current_map, $updater->getMap(), $key);
135
                } catch (\RuntimeException $e) {
136
                    $output->writeln("<comment>Changes to {$desc} mapping:</comment>");
137
                    $output->writeln($e->getMessage());
138
                    $write = true;
139
                }
140
            }
141
        }
142
143
        // Fail on diff if required.
144
        if ($write && $diff && $failOnDiff) {
145
            $io->error('Changes to mapping detected and --fail-on-diff requested, aborting.');
146
            return(2);
147
        }
148
149
        // If changed, save the new map to the PHP file.
150
        if ($write) {
151
            try {
152
                $updater->writeMapToPhpClassFile($current_map->getFileName());
153
                $output->writeln('<comment>Code updated.</comment>');
154
            } catch (\RuntimeException $e) {
155
                $io->error($e->getMessage() .  '.');
156
                return(2);
157
            }
158
        } else {
159
            $output->writeln('<info>No changes to mapping.</info>');
160
        }
161
162
        // Reset the new map's map array.
163
        $updater->getMap()->reset();
164
165
        return(0);
166
    }
167
168
    /**
169
     * Compares two type-to-extension maps by section.
170
     *
171
     * @param MimeMapInterface $old_map
172
     *   The first map to compare.
173
     * @param MimeMapInterface $new_map
174
     *   The second map to compare.
175
     * @param string $section
176
     *   The first-level array key to compare: 't' or 'e' or 'a'.
177
     *
178
     * @throws \RuntimeException with diff details if the maps differ.
179
     *
180
     * @return bool
181
     *   True if the maps are equal.
182
     */
183
    protected function compareMaps(MimeMapInterface $old_map, MimeMapInterface $new_map, string $section): bool
184
    {
185
        $old_map->sort();
186
        $new_map->sort();
187
        $old = $old_map->getMapArray();
188
        $new = $new_map->getMapArray();
189
190
        $factory = new Factory;
191
        $comparator = $factory->getComparatorFor($old[$section], $new[$section]);
192
        try {
193
            $comparator->assertEquals($old[$section], $new[$section]);
194
            return true;
195
        } catch (ComparisonFailure $failure) {
196
            $old_string = var_export($old[$section], true);
197
            $new_string = var_export($new[$section], true);
198
            if (class_exists('\SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder')) {
199
                $differ = new Differ(new UnifiedDiffOutputBuilder("--- Removed\n+++ Added\n"));
200
                throw new \RuntimeException($differ->diff($old_string, $new_string));
201
            } else {
202
                throw new \RuntimeException(' ');
203
            }
204
        }
205
    }
206
}
207