Filesystem::symlink()   B
last analyzed

Complexity

Conditions 6
Paths 13

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 20
cts 20
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 13
nop 2
crap 6
1
<?php
2
3
/*
4
 * This file is part of Rocketeer
5
 *
6
 * (c) Maxime Fabre <[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
 */
12
13
namespace Rocketeer\Services\Connections\Shell\Modules;
14
15
/**
16
 * Files and folders handling.
17
 */
18
class Filesystem extends AbstractBashModule
19
{
20
    ////////////////////////////////////////////////////////////////////
21
    /////////////////////////////// COMMON /////////////////////////////
22
    ////////////////////////////////////////////////////////////////////
23
24
    /**
25
     * Check if a file or folder is a symlink.
26
     *
27
     * @param string $folder
28
     *
29
     * @return bool
30
     */
31 16
    public function isSymlink($folder)
32
    {
33 16
        return $this->checkStatement('-L "'.$folder.'"');
34
    }
35
36
    /**
37
     * Symlinks two folders.
38
     *
39
     * @param string $folder  The folder in shared/
40
     * @param string $symlink The folder that will symlink to it
41
     *
42
     * @return string
0 ignored issues
show
Documentation introduced by Maxime Fabre
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
43
     */
44 118
    public function symlink($folder, $symlink)
45
    {
46 27
        if (!$this->fileExists($folder)) {
47 21
            if (!$this->fileExists($symlink)) {
48 18
                return false;
49
            }
50
51 16
            $this->move($symlink, $folder);
52 118
        }
53
54
        // Switch to relative if required
55 22
        if ($this->config->getContextually('remote.symlink') === 'relative') {
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
56 116
            $folder = $this->paths->computeRelativePathBetween($symlink, $folder);
57 1
        }
58
59 22
        switch ($this->environment->getOperatingSystem()) {
60 22
            case 'Linux':
61 16
                return $this->symlinkSwap($folder, $symlink);
62 6
            default:
63 6
                if ($this->fileExists($symlink)) {
64 6
                    $this->removeFolder($symlink);
65 6
                }
66
67 6
                return $this->modulable->run([
68 6
                    sprintf('ln -s %s %s', $folder, $symlink),
69 6
                ]);
70 6
        }
71
    }
72
73
    /**
74
     * Swap a symlink if possible.
75
     *
76
     * @param string $folder
77
     * @param string $symlink
78
     *
79
     * @return string
80
     */
81 16
    protected function symlinkSwap($folder, $symlink)
82
    {
83 16
        if ($this->fileExists($symlink) && !$this->isSymlink($symlink)) {
84
            $this->removeFolder($symlink);
85
        }
86
87
        // Define name of temporary link
88 16
        $temporary = $symlink.'-temp';
89
90 16
        return $this->modulable->run([
91 16
            sprintf('ln -s %s %s', $folder, $temporary),
92 16
            sprintf('mv -Tf %s %s', $temporary, $symlink),
93 16
        ]);
94
    }
95
96
    /**
97
     * Move a file.
98
     *
99
     * @param string $origin
100
     * @param string $destination
101
     *
102
     * @return string|null
103
     */
104 26
    public function move($origin, $destination)
105
    {
106 22
        if (!$this->fileExists($origin)) {
107 19
            return;
108
        }
109
110 26
        return $this->fromTo('mv', $origin, $destination);
111
    }
112
113
    /**
114
     * Copy a file.
115
     *
116
     * @param string $origin
117
     * @param string $destination
118
     *
119
     * @return string
120
     */
121 3
    public function copy($origin, $destination)
122
    {
123 3
        return $this->fromTo('cp -a', $origin, $destination);
124
    }
125
126
    /**
127
     * Get the contents of a directory.
128
     *
129
     * @param string $directory
130
     *
131
     * @return array
132
     */
133 111
    public function listContents($directory)
134 17
    {
135 111
        $files = $this->modulable->getConnection()->listContents($directory);
136 111
        $files = array_pluck($files, 'path');
137 111
        $files = array_map('basename', $files);
138
139 111
        return $files;
140
    }
141
142
    /**
143
     * Check if a file exists.
144
     *
145
     * @param string $file Path to the file
146
     *
147
     * @return bool
148
     */
149 41
    public function fileExists($file)
150
    {
151 41
        return $this->modulable->getConnection()->has($file);
152
    }
153
154
    /**
155
     * Execute permissions actions on a file with the provided callback.
156
     *
157
     * @param string $folder
158
     *
159
     * @return string
160
     */
161 17
    public function setPermissions($folder)
162
    {
163
        // Get path to folder
164 17
        $folder = $this->releasesManager->getCurrentReleasePath($folder);
165 17
        $this->explainer->line('Setting permissions for '.$folder);
166
167
        // Get permissions options
168 17
        $callback = $this->config->getContextually('remote.permissions.callback');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
169 17
        $commands = (array) $callback($folder, $this);
170
171
        // Cancel if setting of permissions is not configured
172 17
        if (empty($commands)) {
173
            return true;
0 ignored issues
show
Bug Best Practice introduced by Maxime Fabre
The return type of return true; (boolean) is incompatible with the return type documented by Rocketeer\Services\Conne...esystem::setPermissions of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
174
        }
175
176 17
        return $this->modulable->runForCurrentRelease($commands);
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method runForCurrentRelease does not exist on object<Rocketeer\Services\Connections\Shell\Bash>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
177
    }
178
179
    ////////////////////////////////////////////////////////////////////
180
    //////////////////////////////// FILES /////////////////////////////
181
    ////////////////////////////////////////////////////////////////////
182
183
    /**
184
     * Get the contents of a file.
185
     *
186
     * @param string $file
187
     *
188
     * @return string
0 ignored issues
show
Documentation introduced by Maxime Fabre
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
189
     */
190 1
    public function read($file)
191
    {
192 1
        return $this->modulable->getConnection()->read($file);
193
    }
194
195
    /**
196
     * Write to a file.
197
     *
198
     * @param string $file
199
     * @param string $contents
200
     */
201 3
    public function put($file, $contents)
202
    {
203 3
        $this->modulable->getConnection()->put($file, $contents);
204 3
    }
205
206
    /**
207
     * Upload a local file to remote.
208
     *
209
     * @param string      $file
210
     * @param string|null $destination
211
     */
212 2
    public function upload($file, $destination = null)
213
    {
214 2
        if (!file_exists($file)) {
215
            return;
216
        }
217
218
        // Get contents and destination
219 2
        $destination = $destination ?: basename($file);
220
221 2
        $this->put($destination, file_get_contents($file));
222 2
    }
223
224
    /**
225
     * Tail the contents of a file.
226
     *
227
     * @param string $file
228
     * @param bool   $continuous
229
     *
230
     * @return string|null
231
     */
232 1
    public function tail($file, $continuous = true)
233
    {
234 1
        $continuous = $continuous ? ' -f' : null;
235 1
        $command = sprintf('tail %s %s', $file, $continuous);
236
237 1
        return $this->modulable->run($command);
238
    }
239
240
    ////////////////////////////////////////////////////////////////////
241
    /////////////////////////////// FOLDERS ////////////////////////////
242
    ////////////////////////////////////////////////////////////////////
243
244
    /**
245
     * Create a folder in the application's folder.
246
     *
247
     * @param string|null $folder The folder to create
248
     *
249
     * @return string The task
0 ignored issues
show
Documentation introduced by Maxime Fabre
Should the return type not be boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
250
     */
251 11
    public function createFolder($folder = null)
252
    {
253 11
        $folder = $this->paths->getFolder($folder);
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method getFolder does not exist on object<Rocketeer\Services\Environment\Pathfinder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
254 11
        $this->modulable->toHistory('mkdir '.$folder);
255
256 11
        return $this->modulable->getConnection()->createDir($folder);
257
    }
258
259
    /**
260
     * Remove a folder in the application's folder.
261
     *
262
     * @param array|string|null $folders The folder to remove
263
     *
264
     * @return string The task
265
     */
266 13
    public function removeFolder($folders = null)
267
    {
268 13
        $folders = (array) $folders;
269 13
        $folders = array_map([$this->paths, 'getFolder'], $folders);
270 13
        $folders = implode(' ', $folders);
271
272 13
        return $this->modulable->run('rm -rf '.$folders);
273
    }
274
275
    ////////////////////////////////////////////////////////////////////
276
    /////////////////////////////// HELPERS ////////////////////////////
277
    ////////////////////////////////////////////////////////////////////
278
279
    /**
280
     * Check a condition via Bash.
281
     *
282
     * @param string $condition
283
     *
284
     * @return bool
285
     */
286 16
    protected function checkStatement($condition)
287
    {
288 16
        $condition = '[ '.$condition.' ] && echo "true"';
289 16
        $condition = $this->modulable->runRaw($condition);
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method runRaw does not exist on object<Rocketeer\Services\Connections\Shell\Bash>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
290
291 16
        return trim($condition) === 'true';
292
    }
293
294
    /**
295
     * Execute a "from/to" style command.
296
     *
297
     * @param string $command
298
     * @param string $from
299
     * @param string $destination
300
     *
301
     * @return string
302
     */
303 18
    protected function fromTo($command, $from, $destination)
304
    {
305 18
        $folder = dirname($destination);
306 18
        if (!$this->fileExists($folder)) {
307 8
            $this->createFolder($folder);
308 8
        }
309
310 18
        return $this->modulable->run(sprintf('%s %s %s', $command, $from, $destination));
311
    }
312
313
    /**
314
     * @return string[]
315
     */
316 442
    public function getProvided()
317
    {
318
        return [
319 442
            'copy',
320 442
            'createFolder',
321 442
            'fileExists',
322 442
            'isSymlink',
323 442
            'listContents',
324 442
            'move',
325 442
            'put',
326 442
            'read',
327 442
            'removeFolder',
328 442
            'setPermissions',
329 442
            'symlink',
330 442
            'tail',
331 442
            'upload',
332 442
        ];
333
    }
334
}
335