Issues (1)

src/FileResource.php (1 issue)

1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Resume;
4
5
final class FileResource implements Resource
6
{
7
    /** @internal */
8
    const DEFAULT_CHUNK_SIZE = 8192;
9
10
    /**
11
     * Full canonical path of file on local file system
12
     *
13
     * @var string
14
     */
15
    private $localPath;
16
17
    /**
18
     * MIME type of file contents
19
     *
20
     * @var string
21
     */
22
    private $mimeType;
23
24
    /**
25
     * Size of local file, in bytes
26
     *
27
     * @var int
28
     */
29
    private $fileSize;
30
31
    /**
32
     * Stream handle for reading the file
33
     *
34
     * @var resource|null
35
     */
36
    private $handle = null;
37
38
    /**
39
     * Chunk size for local file system reads when sending a partial file
40
     *
41
     * @var int
42
     */
43
    private $chunkSize = self::DEFAULT_CHUNK_SIZE;
44
45
    /**
46
     * Open the local file handle if it's not open yet, and set the pointer to the supplied position
47
     *
48
     * @param int $position
49
     */
50
    private function openFile(int $position)
51
    {
52
        if ($this->handle === null && !$this->handle = \fopen($this->localPath, 'r')) {
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($this->localPath, 'r') can also be of type false. However, the property $handle is declared as type resource|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
53
            throw new SendFileFailureException("Failed to open '{$this->localPath}' for reading");
54
        }
55
56
        if (\fseek($this->handle, $position, \SEEK_SET) !== 0) {
57
            throw new SendFileFailureException('fseek() operation failed');
58
        }
59
    }
60
61
    /**
62
     * Send a chunk of data to the client
63
     *
64
     * @param OutputWriter $outputWriter
65
     * @param int $length
66
     * @return int
67
     */
68
    private function sendDataChunk(OutputWriter $outputWriter, int $length): int
69
    {
70
        $read = $length > $this->chunkSize
71
            ? $this->chunkSize
72
            : $length;
73
74
        $data = \fread($this->handle, $read);
75
76
        if ($data === false) {
77
            throw new SendFileFailureException('fread() operation failed');
78
        }
79
80
        $outputWriter->sendData($data);
81
82
        return \strlen($data);
83
    }
84
85
    /**
86
     * @param string $path Path of file on local file system
87
     * @param string $mimeType MIME type of file contents
88
     * @param int $chunkSize Chunk size for local file system reads when sending a partial file
89
     */
90 3
    public function __construct(string $path, string $mimeType = null, int $chunkSize = self::DEFAULT_CHUNK_SIZE)
91
    {
92 3
        $this->chunkSize = $chunkSize;
93 3
        $this->mimeType = $mimeType ?? 'application/octet-stream';
94
95
        // Make sure the file exists and is a file, otherwise we are wasting our time
96 3
        $this->localPath = \realpath($path);
97
98 3
        if ($this->localPath === false || !\is_file($this->localPath)) {
99 2
            throw new NonExistentFileException("Local path '{$path}' does not exist or is not a file");
100
        }
101
102
        // This shouldn't ever fail but just in case
103 1
        if (false === $this->fileSize = \filesize($this->localPath)) {
104
            throw new UnreadableFileException("Failed to retrieve size of file '{$this->localPath}'");
105
        }
106
    }
107
108
    /**
109
     * Explicitly close the file handle if it's open
110
     */
111 1
    public function __destruct()
112
    {
113 1
        if ($this->handle !== null) {
114
            \fclose($this->handle);
115
        }
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function sendData(OutputWriter $outputWriter, Range $range = null, string $unit = null)
122
    {
123
        if (\strtolower($unit ?? 'bytes') !== 'bytes') {
124
            throw new UnsatisfiableRangeException('Unit not handled by this resource: ' . $unit);
125
        }
126
127
        $start = 0;
128
        $length = $this->fileSize;
129
130
        if ($range !== null) {
131
            $start = $range->getStart();
132
            $length = $range->getLength();
133
        }
134
135
        $this->openFile($start);
136
137
        while ($length > 0) {
138
            $length -= $this->sendDataChunk($outputWriter, $length);
139
        }
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 1
    public function getLength(): int
146
    {
147 1
        return $this->fileSize;
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153
    public function getMimeType(): string
154
    {
155
        return $this->mimeType;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function getAdditionalHeaders(): array
162
    {
163
        return [
164
            'Content-Disposition' => 'attachment; filename="' . \basename($this->localPath) . '"'
165
        ];
166
    }
167
168
    /**
169
     * Get the chunk size for local file system reads when sending a partial file
170
     *
171
     * @return int
172
     */
173
    public function getChunkSize(): int
174
    {
175
        return $this->chunkSize;
176
    }
177
178
    /**
179
     * Set the chunk size for local file system reads when sending a partial file
180
     *
181
     * @param int $chunkSize
182
     */
183
    public function setChunkSize(int $chunkSize)
184
    {
185
        $this->chunkSize = $chunkSize;
186
    }
187
}
188