InstallTask::doPerform()   A
last analyzed

Complexity

Conditions 3
Paths 15

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 3
eloc 14
nc 15
nop 0
1
<?php
2
3
/**
4
 * This file is part of tenside/core.
5
 *
6
 * (c) Christian Schiffler <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * This project is provided in good faith and hope to be usable by anyone.
12
 *
13
 * @package    tenside/core
14
 * @author     Christian Schiffler <[email protected]>
15
 * @author     Andreas Schempp <[email protected]>
16
 * @copyright  2015 Christian Schiffler <[email protected]>
17
 * @license    https://github.com/tenside/core/blob/master/LICENSE MIT
18
 * @link       https://github.com/tenside/core
19
 * @filesource
20
 */
21
22
namespace Tenside\Core\Task\Composer;
23
24
use Composer\IO\IOInterface;
25
use Symfony\Component\Console\Input\ArrayInput;
26
use Symfony\Component\Finder\Finder;
27
use Symfony\Component\Finder\SplFileInfo;
28
use Tenside\Core\Task\Composer\WrappedCommand\CreateProjectCommand;
29
30
/**
31
 * This class holds the information for a fresh installation of a project (composer create-project).
32
 */
33
class InstallTask extends AbstractComposerCommandTask
34
{
35
    /**
36
     * Constant for the package name key.
37
     */
38
    const SETTING_PACKAGE = 'package';
39
40
    /**
41
     * Constant for the package version key.
42
     */
43
    const SETTING_VERSION = 'version';
44
45
    /**
46
     * Constant for the destination dir key.
47
     */
48
    const SETTING_DESTINATION_DIR = 'dest-dir';
49
50
    /**
51
     * Constant for the repository url.
52
     */
53
    const SETTING_REPOSITORY_URL = 'repository-url';
54
55
    /**
56
     * The temporary directory to use.
57
     *
58
     * @var string
59
     */
60
    private $tempDir;
61
62
    /**
63
     * The environment variable storage.
64
     *
65
     * @var string|null
66
     */
67
    private $previousEnvVariable;
68
69
    /**
70
     * The previous working directory.
71
     *
72
     * @var string
73
     */
74
    private $previousWorkingDir;
75
76
    /**
77
     * The list of folders to remove after the installation was complete.
78
     *
79
     * @var string[]
80
     */
81
    private $folders;
82
83
    /**
84
     * {@inheritDoc}
85
     */
86
    public function getType()
87
    {
88
        return 'install';
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     *
94
     * @throws \RuntimeException When the project directory is not empty or when the installation was not successful.
95
     */
96
    public function doPerform()
97
    {
98
        if (!$this->mayInstall()) {
99
            throw new \RuntimeException('Project directory not empty.');
100
        }
101
102
        // Will throw exception upon error.
103
        $this->prepareTmpDir();
104
105
        $this->preserveEnvironment();
106
107
        try {
108
            parent::doPerform();
109
            $this->moveFiles();
110
        } catch (\Exception $exception) {
111
            $this->restoreEnvironment();
112
            throw new \RuntimeException('Project could not be created.', 1, $exception);
113
        } finally {
114
            $this->restoreEnvironment();
115
        }
116
117
        rmdir($this->tempDir);
118
    }
119
120
    /**
121
     * Prepare a temporary directory.
122
     *
123
     * @return void
124
     *
125
     * @throws \RuntimeException When an error occurred.
126
     */
127
    private function prepareTmpDir()
128
    {
129
        $tempDir = $this->file->get(self::SETTING_DESTINATION_DIR) . DIRECTORY_SEPARATOR . uniqid('install-');
130
131
        // If the temporary folder could not be created, error out.
132
        // @codingStandardsIgnoreStart - we silence on purpose here as we create an exception on error.
133
        if (!@mkdir($tempDir, 0700)) {
134
        // @codingStandardsIgnoreEnd
135
            throw new \RuntimeException('Could not create the temporary directory');
136
        }
137
138
        $this->tempDir = $tempDir;
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    protected function prepareCommand()
145
    {
146
        return $this->attachComposerFactory(new CreateProjectCommand());
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152
    protected function prepareInput()
153
    {
154
        $arguments = [
155
            'package'   => $this->file->get(self::SETTING_PACKAGE),
156
            'directory' => $this->tempDir,
157
            '--prefer-dist',
158
            '--no-dev',
159
            '--no-interaction'
160
        ];
161
162
        if ($version = $this->file->get(self::SETTING_VERSION)) {
163
            $arguments['version'] = $version;
164
        }
165
166
        if ($repository = $this->file->get(self::SETTING_REPOSITORY_URL)) {
167
            $arguments['--repository-url'] = $repository;
168
        }
169
170
        $input = new ArrayInput($arguments);
171
172
        $input->setInteractive(false);
173
174
        return $input;
175
    }
176
177
    /**
178
     * Move the installed files to their intended destination.
179
     *
180
     * @return void
181
     */
182
    private function moveFiles()
183
    {
184
        // Ensure we have the file permissions not in cache as new files were installed.
185
        clearstatcache();
186
        // Now move all the files over.
187
        $destinationDir = $this->file->get(self::SETTING_DESTINATION_DIR);
188
        $ioHandler      = $this->getIO();
189
        $logging        = $ioHandler->isVeryVerbose();
190
        $this->folders  = [];
191
        foreach (Finder::create()->in($this->tempDir)->ignoreDotFiles(false)->ignoreVCS(false) as $file) {
192
            $this->moveFile($file, $destinationDir, $logging, $ioHandler);
193
        }
194
195
        foreach (array_reverse($this->folders) as $folder) {
196
            if ($logging) {
197
                $ioHandler->write(sprintf('remove directory %s', $folder));
198
            }
199
            rmdir($folder);
200
        }
201
    }
202
203
    /**
204
     * Move a single file or folder.
205
     *
206
     * @param SplFileInfo $file      The file to move.
207
     *
208
     * @param string      $targetDir The destination directory.
209
     *
210
     * @param bool        $logging   Flag determining if actions shall get logged.
211
     *
212
     * @param IOInterface $ioHandler The io handler to log to.
213
     *
214
     * @return void
215
     *
216
     * @throws \RuntimeException When an unknown file type has been encountered.
217
     */
218
    private function moveFile(SplFileInfo $file, $targetDir, $logging, $ioHandler)
219
    {
220
        $pathName        = $file->getPathname();
221
        $destinationFile = str_replace($this->tempDir, $targetDir, $pathName);
222
223
        // Symlink must(!) be handled first as the isDir() and isFile() checks return true for symlinks.
224
        if ($file->isLink()) {
225
            $target = $file->getLinkTarget();
226
            if ($logging) {
227
                $ioHandler->write(sprintf('link %s to %s', $target, $destinationFile));
228
            }
229
            symlink($target, $destinationFile);
230
            unlink($pathName);
231
232
            return;
233
        }
234
235
        if ($file->isDir()) {
236
            $permissions     = substr(decoct(fileperms($pathName)), 1);
237
            $this->folders[] = $pathName;
238
            if (!is_dir($destinationFile)) {
239
                if ($logging) {
240
                    $ioHandler->write(sprintf('mkdir %s (permissions: %s)', $pathName, $permissions));
241
                }
242
                mkdir($destinationFile, octdec($permissions), true);
243
            }
244
245
            return;
246
        }
247
248
        if ($file->isFile()) {
249
            $permissions = substr(decoct(fileperms($pathName)), 1);
250
            if ($logging) {
251
                $ioHandler->write(
252
                    sprintf('move %s to %s (permissions: %s)', $pathName, $destinationFile, $permissions)
253
                );
254
            }
255
            copy($pathName, $destinationFile);
256
            chmod($destinationFile, octdec($permissions));
257
            unlink($pathName);
258
259
            return;
260
        }
261
262
        throw new \RuntimeException(
263
            sprintf(
264
                'Unknown file of type %s encountered for %s',
265
                filetype($pathName),
266
                $pathName
267
            )
268
        );
269
    }
270
271
    /**
272
     * Check if we may install into the destination directory.
273
     *
274
     * @return bool
275
     */
276
    private function mayInstall()
277
    {
278
        $destinationDir = $this->file->get(self::SETTING_DESTINATION_DIR) . DIRECTORY_SEPARATOR;
279
280
        return !(file_exists($destinationDir . 'composer.json'));
281
    }
282
283
    /**
284
     * Save the current environment variable and working directory.
285
     *
286
     * @return void
287
     */
288
    private function preserveEnvironment()
289
    {
290
        $this->previousEnvVariable = getenv('COMPOSER');
291
        $this->previousWorkingDir  = getcwd();
292
        // Clear any potential overriding env variable.
293
        putenv('COMPOSER=');
294
    }
295
296
    /**
297
     * Restore the current environment variable and working directory.
298
     *
299
     * @return void
300
     */
301
    private function restoreEnvironment()
302
    {
303
        putenv('COMPOSER=' . $this->previousEnvVariable);
304
        chdir($this->previousWorkingDir);
305
    }
306
}
307