WritableTrait::delete()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
ccs 0
cts 7
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 2
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Storage\Bucket;
6
7
use JetBrains\PhpStorm\ExpectedValues;
8
use League\Flysystem\FilesystemException;
9
use League\Flysystem\FilesystemOperator;
10
use Spiral\Storage\Exception\FileOperationException;
11
use Spiral\Storage\FileInterface;
12
use Spiral\Storage\BucketInterface;
13
use Spiral\Storage\Visibility;
14
15
/**
16
 * @mixin WritableInterface
17
 */
18
trait WritableTrait
19
{
20
    public function create(string $pathname, array $config = []): FileInterface
21
    {
22
        if ($this instanceof ReadableInterface && !$this->exists($pathname)) {
23
            return $this->write($pathname, '', $config);
24
        }
25
26
        return $this->file($pathname);
0 ignored issues
show
Bug introduced by
It seems like file() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

26
        return $this->/** @scrutinizer ignore-call */ file($pathname);
Loading history...
27
    }
28
29
    public function write(string $pathname, mixed $content, array $config = []): FileInterface
30
    {
31
        \assert(\is_resource($content) || $this->isStringable($content));
32
33
        $fs = $this->getOperator();
34
35
        try {
36
            switch (true) {
37
                case \is_object($content):
0 ignored issues
show
Unused Code introduced by
is_object($content) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
38
                case \is_string($content):
39
                    $fs->write($pathname, (string) $content, $config);
40
                    break;
41
42
                case \is_resource($content):
43
                    $fs->writeStream($pathname, $content, $config);
44
                    break;
45
46
                default:
47
                    $message = 'Content must be a resource stream or stringable type, but %s passed';
48
                    throw new \InvalidArgumentException(\sprintf($message, \get_debug_type($content)));
49
            }
50
        } catch (FilesystemException $e) {
51
            throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
52
        }
53
54
        return $this->file($pathname);
55
    }
56
57
    public function setVisibility(
58
        string $pathname,
59
        #[ExpectedValues(valuesFromClass: Visibility::class)]
60
        string $visibility,
61
    ): FileInterface {
62
        $fs = $this->getOperator();
63
64
        try {
65
            $fs->setVisibility($pathname, $this->toFlysystemVisibility($visibility));
66
        } catch (FilesystemException $e) {
67
            throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
68
        }
69
70
        return $this->file($pathname);
71
    }
72
73
    public function copy(
74
        string $source,
75
        string $destination,
76
        ?BucketInterface $storage = null,
77
        array $config = [],
78
    ): FileInterface {
79
        $fs = $this->getOperator();
80
81
        if ($storage === null || $storage === $this) {
82
            try {
83
                $fs->copy($source, $destination, $config);
84
            } catch (FilesystemException $e) {
85
                throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
86
            }
87
88
            return $this->file($destination);
89
        }
90
91
        return $storage->write($destination, $this->getStream($source), $config);
0 ignored issues
show
Bug introduced by
It seems like getStream() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
        return $storage->write($destination, $this->/** @scrutinizer ignore-call */ getStream($source), $config);
Loading history...
92
    }
93
94
    public function move(
95
        string $source,
96
        string $destination,
97
        ?BucketInterface $storage = null,
98
        array $config = [],
99
    ): FileInterface {
100
        $fs = $this->getOperator();
101
102
        if ($storage === null || $storage === $this) {
103
            try {
104
                $fs->move($source, $destination, $config);
105
            } catch (FilesystemException $e) {
106
                throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
107
            }
108
109
            return $this->file($destination);
110
        }
111
112
        $result = $storage->write($destination, $this->getStream($source), $config);
113
114
        $fs->delete($source);
115
116
        return $result;
117
    }
118
119
    public function delete(string $pathname, bool $clean = false): void
120
    {
121
        $fs = $this->getOperator();
122
123
        try {
124
            $fs->delete($pathname);
125
126
            if ($clean) {
127
                /** @psalm-suppress InternalMethod */
128
                $this->deleteEmptyDirectories($this->getParentDirectory($pathname));
129
            }
130
        } catch (FilesystemException $e) {
131
            throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
132
        }
133
    }
134
135
    abstract protected function getOperator(): FilesystemOperator;
136
137
    #[ExpectedValues(valuesFromClass: \League\Flysystem\Visibility::class)]
138
    private function toFlysystemVisibility(
139
        #[ExpectedValues(valuesFromClass: Visibility::class)]
140
        string $visibility,
141
    ): string {
142
        return ($visibility === Visibility::VISIBILITY_PUBLIC)
143
            ? \League\Flysystem\Visibility::PUBLIC
144
            : \League\Flysystem\Visibility::PRIVATE;
145
    }
146
147
    /**
148
     * Internal helper method that returns directory name of passed path.
149
     *
150
     * Please note that the use of the PHP {@see \dirname()} function depends
151
     * on the operating system and it MAY NOT return correct parent directory
152
     * in the case of slash character (`/` or `\`) incompatible with the
153
     * current runtime.
154
     *
155
     * @internal This is an internal method, please do not use it in your code.
156
     * @psalm-internal Spiral\Storage\Storage
157
     */
158
    private function getParentDirectory(string $path): string
159
    {
160
        return \dirname(\str_replace(['\\', '/'], \DIRECTORY_SEPARATOR, $path));
161
    }
162
163
    /**
164
     * Internal helper method that returns bool {@see true} if the passed
165
     * directory is the root for the file.
166
     *
167
     * @internal This is an internal method, please do not use it in your code.
168
     * @psalm-internal Spiral\Storage\Storage
169
     */
170
    private function hasParentDirectory(string $directory): bool
171
    {
172
        return $directory !== '' && $directory !== '.';
173
    }
174
175
    /**
176
     * Internal helper method that recursively deletes empty directories.
177
     *
178
     * @internal This is an internal method, please do not use it in your code.
179
     * @psalm-internal Spiral\Storage\Storage
180
     * @psalm-suppress InternalMethod
181
     *
182
     * @throws FileOperationException
183
     */
184
    private function deleteEmptyDirectories(string $directory): void
185
    {
186
        if (!$this->hasParentDirectory($directory)) {
187
            return;
188
        }
189
190
        $fs = $this->getOperator();
191
192
        try {
193
            if (!$this->hasFiles($directory)) {
194
                $fs->deleteDirectory($directory);
195
                $this->deleteEmptyDirectories($this->getParentDirectory($directory));
196
            }
197
        } catch (FilesystemException $e) {
198
            throw new FileOperationException($e->getMessage(), $e->getCode(), $e);
199
        }
200
    }
201
202
    /**
203
     * Internal helper method that returns bool {@see true} if directory
204
     * not empty.
205
     *
206
     * Note: Be careful, this method can be quite slow as it asks for a
207
     * list of files from filesystem.
208
     *
209
     * @internal This is an internal method, please do not use it in your code.
210
     * @psalm-internal Spiral\Storage\Storage
211
     *
212
     * @throws FilesystemException
213
     */
214
    private function hasFiles(string $directory): bool
215
    {
216
        $fs = $this->getOperator();
217
218
        foreach ($fs->listContents($directory) as $_) {
219
            return true;
220
        }
221
222
        return false;
223
    }
224
225
    /**
226
     * Internal helper method that returns bool {@see true} if passed argument
227
     * can be converted to string.
228
     */
229
    private function isStringable(mixed $value): bool
230
    {
231
        return match (true) {
232
            \is_string($value) => true,
233
            !\is_object($value) => false,
234
            default => $value instanceof \Stringable,
235
        };
236
    }
237
}
238