ZIPHelper::save()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 10
c 1
b 1
f 0
dl 0
loc 19
rs 9.9332
cc 2
nc 2
nop 0
1
<?php
2
/**
3
 * File containing the {@link ZIPHelper} class.
4
 * @package Application Utils
5
 * @subpackage ZIPHelper
6
 * @see ZIPHelper
7
 */
8
9
namespace AppUtils;
10
11
use AppUtils\ConvertHelper\JSONConverter;
12
use AppUtils\ConvertHelper\JSONConverter\JSONConverterException;
13
use ZipArchive;
14
15
/**
16
 * ZIP helper class to simplify working with the native 
17
 * PHP ZIPArchive functions.
18
 * 
19
 * Usage:
20
 * 
21
 * <pre>
22
 * $zip = new ZIPHelper('ziparchive.zip');
23
 * </pre>
24
 * 
25
 * @package Application Utils
26
 * @subpackage ZIPHelper
27
 * @author Sebastian Mordziol <[email protected]>
28
 */
29
class ZIPHelper
30
{
31
    public const ERROR_SOURCE_FILE_DOES_NOT_EXIST = 338001;
32
    public const ERROR_NO_FILES_IN_ARCHIVE = 338002;
33
    public const ERROR_OPENING_ZIP_FILE = 338003;
34
    public const ERROR_CANNOT_SAVE_FILE_TO_DISK =338004;
35
36
    /**
37
     * @var array<string,mixed>
38
     */
39
    protected array $options = array(
40
        'WriteThreshold' => 100
41
    );
42
    
43
   /**
44
    * @var string
45
    */
46
    protected $file;
47
    
48
   /**
49
    * @var ZipArchive|NULL
50
    */
51
    protected $zip;
52
53
    protected bool $open = false;
54
55
    public function __construct(string $targetFile)
56
    {
57
        $this->file = $targetFile;
58
    }
59
    
60
   /**
61
    * Sets an option, among the available options:
62
    * 
63
    * <ul>
64
    * <li>WriteThreshold: The amount of files to add before the zip is automatically written to disk and re-opened to release the file handles. Set to 0 to disable.</li>
65
    * </ul>
66
    * 
67
    * @param string $name
68
    * @param mixed $value
69
    * @return ZIPHelper
70
    */
71
    public function setOption(string $name, $value) : ZIPHelper
72
    {
73
        $this->options[$name] = $value;
74
        return $this;
75
    }
76
    
77
   /**
78
    * Adds a file to the zip. By default, the file is stored
79
    * with the same name in the root of the zip. Use the optional
80
    * parameter to change the location in the zip.
81
    * 
82
    * @param string $filePath
83
    * @param string|null $zipPath If no path is specified, file will be added with the same name in the ZIP's root.
84
    * @throws ZIPHelper_Exception
85
    * @return bool
86
    * 
87
    * @see FileHelper::ERROR_SOURCE_FILE_DOES_NOT_EXIST
88
    */
89
    public function addFile(string $filePath, ?string $zipPath=null) : bool
90
    {
91
        $this->open();
92
        
93
        if (!file_exists($filePath) || !is_file($filePath)) 
94
        {
95
            throw new ZIPHelper_Exception(
96
                'File not found or not a file',
97
                sprintf(
98
                    'Tried adding the file [%1$s] to the zip file, but it does not exist, or it is not a file.',
99
                    $filePath
100
                ),
101
                self::ERROR_SOURCE_FILE_DOES_NOT_EXIST
102
            );
103
        }
104
        
105
        if (empty($zipPath)) {
106
            $zipPath = basename($filePath);
107
        }
108
        
109
        $result = $this->zip->addFile($filePath, $zipPath);
0 ignored issues
show
Bug introduced by
The method addFile() does not exist on null. ( Ignorable by Annotation )

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

109
        /** @scrutinizer ignore-call */ 
110
        $result = $this->zip->addFile($filePath, $zipPath);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
110
        
111
        $this->releaseFileHandles();
112
        
113
        return $result;
114
    }
115
116
    /**
117
     * Uses the specified string as content for a file to add to
118
     * the archive.
119
     *
120
     * @param string $contents The contents of the file.
121
     * @param string $zipPath The filename, or relative path within the archive.
122
     * @return bool
123
     */
124
    public function addString(string $contents, string $zipPath) : bool
125
    {
126
        $this->open();
127
        
128
        return $this->zip->addFromString($zipPath, $contents);
129
    }
130
131
    /**
132
     * @throws ZIPHelper_Exception
133
     * @see ZIPHelper::ERROR_OPENING_ZIP_FILE
134
     */
135
    protected function open() : void
136
    {
137
        if($this->open) {
138
            return;
139
        }
140
        
141
        if(!isset($this->zip)) {
142
            $this->zip = new ZipArchive();
143
        }
144
        
145
        $flag = null;
146
        if(!file_exists($this->file)) {
147
            $flag = ZipArchive::CREATE;
148
        }
149
        
150
        if ($this->zip->open($this->file, $flag) !== true) {
151
            throw new ZIPHelper_Exception(
152
                'Cannot open ZIP file',
153
                sprintf(
154
                    'Opening the ZIP file [%1$s] failed.',
155
                    $this->file
156
                ),
157
                self::ERROR_OPENING_ZIP_FILE
158
            );
159
        }
160
        
161
        $this->open = true;
162
    }
163
164
    /**
165
     * @var int
166
     */
167
    protected $fileTracker = 0;
168
169
    /**
170
     * Checks whether the file handles currently open for the
171
     * zip file have to be released. This is called for every
172
     * file that gets added to the file.
173
     *
174
     * With a large amount of files being added to the zip file, it is
175
     * possible to reach the limit of the amount of file handles open
176
     * at the same time: This is because PHP locks every file that gets
177
     * added to the ZIP, until the ZIP is written to disk.
178
     *
179
     * To counter this problem, we simply write the ZIP every X files
180
     * added to it, so the file handles get released.
181
     *
182
     * @see addFileToZip()
183
     * @see $zipWriteThreshold
184
     */
185
    protected function releaseFileHandles() : void
186
    {
187
        $this->fileTracker++;
188
189
        if($this->options['WriteThreshold'] < 1) {
190
            return;
191
        }
192
        
193
        if ($this->fileTracker >= $this->options['WriteThreshold']) {
194
            $this->close();
195
            $this->open();
196
            $this->fileTracker = 0;
197
        }
198
    }
199
200
    /**
201
     * @return void
202
     * @throws ZIPHelper_Exception
203
     */
204
    protected function close() : void
205
    {
206
        if(!$this->open) {
207
            return;
208
        }
209
        
210
        if (!$this->zip->close()) 
211
        {
212
            throw new ZIPHelper_Exception(
213
                'Could not save ZIP file to disk',
214
                sprintf(
215
                    'Tried saving the ZIP file [%1$s], but the write failed. This can have several causes, ' .
216
                    'including adding files that do not exist on disk, trying to create an empty zip, ' .
217
                    'or trying to save to a directory that does not exist.',
218
                    $this->file
219
                ),
220
                self::ERROR_CANNOT_SAVE_FILE_TO_DISK
221
            );
222
        }
223
        
224
        $this->open = false;
225
    }
226
227
    /**
228
     * @return $this
229
     * @throws ZIPHelper_Exception
230
     */
231
    public function save() : ZIPHelper
232
    {
233
        $this->open();
234
        
235
        if($this->countFiles() < 1) 
236
        {
237
            throw new ZIPHelper_Exception(
238
                'No files in the zip file',
239
                sprintf(
240
                    'No files were added to the zip file [%1$s], cannot save it without any files.',
241
                    $this->file
242
                ),
243
                self::ERROR_NO_FILES_IN_ARCHIVE
244
            );
245
        }
246
        
247
        $this->close();
248
249
        return $this;
250
    }
251
252
    /**
253
     * Writes the active ZIP file to disk, and sends headers for
254
     * the client to download it.
255
     *
256
     * @param string|NULL $fileName Override the ZIP's file name for the download
257
     * @see ZIPHelper::downloadAndDelete()
258
     * @throws ZIPHelper_Exception
259
     * @return string The file name that was sent (useful in case none was specified).
260
     */
261
    public function download(?string $fileName=null) : string
262
    {
263
        $this->save();
264
        
265
        if(empty($fileName))
266
        {
267
            $fileName = basename($this->file);
268
        }
269
        
270
        header('Content-type: application/zip');
271
        header('Content-Disposition: attachment; filename=' . $fileName);
272
        header('Content-length: ' . filesize($this->file));
273
        header('Pragma: no-cache');
274
        header('Expires: 0');
275
        readfile($this->file);
276
        
277
        return $fileName;
278
    }
279
280
    /**
281
     * Like {@link ZIPHelper::download()}, but deletes the
282
     * file after sending it to the browser.
283
     *
284
     * @param string|NULL $fileName Override the ZIP's file name for the download
285
     * @return $this
286
     * @throws FileHelper_Exception
287
     * @throws ZIPHelper_Exception
288
     * @see ZIPHelper::download()
289
     */
290
    public function downloadAndDelete(?string $fileName=null) : ZIPHelper
291
    {
292
        $this->download($fileName);
293
        
294
        FileHelper::deleteFile($this->file);
295
296
        return $this;
297
    }
298
299
    /**
300
     * Extracts all files and folders from the zip to the
301
     * target folder. If no folder is specified, the files
302
     * are extracted into the same folder as the zip itself.
303
     *
304
     * @param string|NULL $outputFolder If no folder is specified, uses the target file's folder.
305
     * @return boolean
306
     * @throws ZIPHelper_Exception
307
     */
308
    public function extractAll(?string $outputFolder=null) : bool
309
    {
310
        if(empty($outputFolder)) {
311
            $outputFolder = dirname($this->file);
312
        }
313
        
314
        $this->open();
315
        
316
        return $this->zip->extractTo($outputFolder);
317
    }
318
319
    /**
320
     * @return ZipArchive
321
     * @throws ZIPHelper_Exception
322
     */
323
    public function getArchive() : ZipArchive
324
    {
325
        $this->open();
326
        
327
        return $this->zip;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->zip could return the type null which is incompatible with the type-hinted return ZipArchive. Consider adding an additional type-check to rule them out.
Loading history...
328
    }
329
330
    /**
331
     * JSON encodes the specified data and adds the json as
332
     * a file in the ZIP archive.
333
     *
334
     * @param mixed $data
335
     * @param string $zipPath
336
     * @return boolean
337
     *
338
     * @throws JSONConverterException
339
     */
340
    public function addJSON($data, string $zipPath) : bool
341
    {
342
        return $this->addString(
343
            JSONConverter::var2json($data),
344
            $zipPath
345
        );
346
    }
347
348
    /**
349
     * Counts the amount of files currently present in the archive.
350
     * @return int
351
     * @throws ZIPHelper_Exception
352
     */
353
    public function countFiles() : int
354
    {
355
        $this->open();
356
        
357
        return (int)$this->zip->numFiles;
358
    }
359
}
360