Builder::dropFirstLine()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 1
nop 0
dl 0
loc 15
ccs 10
cts 10
cp 1
crap 2
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * pipelines - run bitbucket pipelines wherever they dock
5
 *
6
 * Copyright 2017, 2018 Tom Klingenberg <[email protected]>
7
 *
8
 * Licensed under GNU Affero General Public License v3.0 or later
9
 */
10
11
namespace Ktomk\Pipelines\PharBuild;
12
13
use DateTime;
14
use Ktomk\Pipelines\Glob;
15
use Ktomk\Pipelines\Lib;
16
use Phar;
17
18
class Builder
19
{
20
    /**
21
     * public to allow injection in tests
22
     *
23
     * @var null|resource to write errors to (if not set, standard error)
24
     */
25
    public $errHandle;
26
27
    /**
28
     * @var string path of the phar file to build
29
     */
30
    private $fPhar;
31
32
    /**
33
     * @var array to collect files to build the phar from $localName => $descriptor
34
     */
35
    private $files;
36
37
    /**
38
     * @var string
39
     */
40
    private $stub;
41
42
    /**
43
     * @var array
44
     */
45
    private $errors;
46
47
    /**
48
     * @var int directory depth limit for ** glob pattern
49
     */
50
    private $limit;
51
52
    /**
53
     * @var string pre-generated replacement pattern for self::$limit
54
     */
55
    private $double;
56
57
    /**
58
     * @var array keep file path (as key) to be unlinked on __destruct() (housekeeping)
59
     */
60
    private $unlink = array();
61
62
    /**
63
     * @param string $fphar phar file name
64
     *
65
     * @return Builder
66
     */
67 10
    public static function create($fphar)
68
    {
69 10
        umask(022);
70
71 10
        $builder = new self();
72 10
        $builder->_ctor($fphar);
73
74 10
        return $builder;
75
    }
76
77 3
    public function __destruct()
78
    {
79 3
        foreach ($this->unlink as $path => $test) {
80 1
            if (file_exists($path) && unlink($path)) {
81 1
                unset($this->unlink[$path]);
82
            }
83
        }
84
    }
85
86
    /**
87
     * @param string $file
88
     *
89
     * @throws \RuntimeException
90
     *
91
     * @return $this
92
     */
93 3
    public function stubfile($file)
94
    {
95 3
        unset($this->stub);
96
97 3
        if (!$buffer = file_get_contents($file)) {
98 1
            $this->err(sprintf('error reading stubfile: %s', $file));
99
        } else {
100 2
            $this->stub = $buffer;
101
        }
102
103 3
        return $this;
104
    }
105
106
    /**
107
     * set traversal limit for double-dot glob '**'
108
     *
109
     * @param int $limit 0 to 16, 0 makes '**' effectively act like '*' for path segments
110
     *
111
     * @return $this
112
     */
113 10
    public function limit($limit)
114
    {
115 10
        $limit = (int)min(16, max(0, $limit));
116 10
        $this->limit = $limit;
117 10
        $this->double = $limit
118 10
            ? str_repeat('{', $limit) . str_repeat('*/,}', $limit) . '*'
119 2
            : '*';
120
121 10
        return $this;
122
    }
123
124
    /**
125
     * add files to build the phar archive of
126
     *
127
     * @param string|string[] $pattern one or more patterns to add as relative files
128
     * @param callable $callback [optional] to apply on each file found
129
     * @param string $directory [optional] where to add from
130
     * @param string $alias [optional] prefix local names
131
     *
132
     * @throws \RuntimeException
133
     *
134
     * @return $this|Builder
135
     */
136 4
    public function add($pattern, $callback = null, $directory = null, $alias = null)
137
    {
138 4
        if (null !== $directory) {
139 2
            $result = realpath($directory);
140 2
            if (false === $result || !is_dir($result)) {
141 1
                $this->err(sprintf('invalid directory: %s', $directory));
142
143 1
                return $this;
144
            }
145 2
            $directory = $result . '/';
146
        }
147
148 4
        if (null !== $alias) {
149 2
            $result = trim($alias, '/');
150 2
            if ('' === $result) {
151 1
                $this->err(sprintf(
152
                    '%s: ineffective alias: %s',
153 1
                    is_array($pattern) ? implode(';', $pattern) : $pattern,
154
                    $alias
155
                ));
156 1
                $alias = null;
157
            } else {
158 1
                $alias = $result . '/';
159
            }
160
        }
161
162 4
        foreach ((array)$pattern as $one) {
163 4
            $this->_add($one, $callback, (string)$directory, (string)$alias);
164
        }
165
166 4
        return $this;
167
    }
168
169
    /**
170
     * Take a snapshot of the file when added to the build, makes
171
     * it immune to later content changes.
172
     *
173
     * @throws \RuntimeException
174
     *
175
     * @return \Closure
176
     */
177 1
    public function snapShot()
178
    {
179 1
        $self = $this;
180 1
        $unlink = &$this->unlink;
181
182 1
        return function ($file) use ($self, &$unlink) {
183 1
            $source = fopen($file, 'rb');
184 1
            if (false === $source) {
185 1
                $self->err(sprintf('failed to open for reading: %s', $file));
186
187 1
                return null;
188
            }
189
190 1
            $target = tmpfile();
191 1
            if (false === $target) {
192
                // @codeCoverageIgnoreStart
193
                fclose($source);
194
                $self->err(sprintf('failed to open temp file for writing'));
195
196
                return null;
197
                // @codeCoverageIgnoreEnd
198
            }
199
200 1
            $meta = stream_get_meta_data($target);
201 1
            $snapShotFile = $meta['uri'];
202
203 1
            if (false === (bool)stream_copy_to_stream($source, $target)) {
204
                // @codeCoverageIgnoreStart
205
                $self->err(sprintf('stream copy error: %s', $file));
206
                fclose($source);
207
                fclose($target);
208
                unlink($snapShotFile);
209
210
                return null;
211
                // @codeCoverageIgnoreEnd
212
            }
213 1
            fclose($source);
214
215
            # preserve file from deletion until later cleanup
216 1
            $unlink[$snapShotFile] = $target;
217
218 1
            return array('fil', $snapShotFile);
219
        };
220
    }
221
222
    /**
223
     * Drop first line from file when added to the build, e.g.
224
     * for removing a shebang line.
225
     *
226
     * @throws \RuntimeException
227
     *
228
     * @return \Closure
229
     */
230 3
    public function dropFirstLine()
231
    {
232 3
        $self = $this;
233
234 3
        return function ($file) use ($self) {
235 3
            $lines = file($file);
236 3
            if (false === $lines) {
237 1
                $self->err(sprintf('error reading file: %s', $file));
238
239 1
                return null;
240
            }
241 2
            array_shift($lines);
242 2
            $buffer = implode('', $lines);
243
244 2
            return array('str', $buffer);
245
        };
246
    }
247
248
    /**
249
     * String replace on file contents
250
     *
251
     * @param string $that
252
     * @param string $with
253
     *
254
     * @return \Closure
255
     */
256 1
    public function replace($that, $with)
257
    {
258 1
        return function ($file) use ($that, $with) {
259 1
            $buffer = file_get_contents($file);
260 1
            $buffer = strtr($buffer, array($that => $with));
261
262 1
            return array('str', $buffer);
263
        };
264
    }
265
266
    /**
267
     * build phar file and optionally invoke it with parameters for
268
     * a quick smoke test
269
     *
270
     * @param string $params [options]
271
     *
272
     * @throws \RuntimeException
273
     * @throws \UnexpectedValueException
274
     * @throws \BadMethodCallException
275
     *
276
     * @return $this
277
     */
278 6
    public function build($params = null)
279
    {
280 6
        $file = $this->fPhar;
281 6
        $files = $this->files;
282
283 6
        $temp = $this->_tempname('.phar');
284 6
        if (false === $temp) {
285
            // @codeCoverageIgnoreStart
286
            $this->err('fatal: failed to create tmp phar archive file');
287
288
            return $this;
289
            // @codeCoverageIgnoreEnd
290
        }
291
292 6
        if (file_exists($file) && !unlink($file)) {
293 1
            $this->err(sprintf("could not unlink existing file '%s'", $file));
294
295 1
            return $this;
296
        }
297
298 5
        if (!Phar::canWrite()) {
299 1
            $this->err("phar: writing phar files is disabled by the php.ini setting 'phar.readonly'");
300
        }
301
302 5
        if (empty($files)) {
303 1
            $this->err('no files, add some or do not remove all');
304
        }
305
306 5
        if (!empty($this->errors)) {
307 3
            $this->err('fatal: build has errors, not building');
308
309 3
            return $this;
310
        }
311
312 2
        $phar = new Phar($temp);
313 2
        $phar->startBuffering();
314
315 2
        if (null !== $this->stub) {
316 1
            $phar->setStub($this->stub);
317
        }
318
319 2
        $count = $this->_bfiles($phar, $files);
320 2
        if (count($files) !== $count) {
321 1
            $this->err(sprintf('only %d of %d files could be added', $count, count($files)));
322
        }
323
324 2
        $phar->stopBuffering();
325 2
        unset($phar); # save file
326
327 2
        if (0 === $count) {
328 1
            $this->err('fatal: no files in phar archive, must have at least one');
329
330 1
            return $this;
331
        }
332
333 1
        copy($temp, $file);
334
335
        # chmod +x for lazy ones
336 1
        if (!chmod($file, 0775)) {
337
            // @codeCoverageIgnoreStart
338
            $this->err('error changing mode to 0775 on phar file');
339
            // @codeCoverageIgnoreEnd
340
        }
341
342
        # smoke test TODO operate on secondary temp file, execution options
343 1
        if (null !== $params) {
344 1
            $this->exec(sprintf('./%s %s', $file, $params), $return);
345 1
            printf("%s\n", $return);
346
        }
347
348 1
        return $this;
349
    }
350
351
    /**
352
     * updates each file's unix timestamps in the phar archive,
353
     * useful for reproducible builds
354
     *
355
     * @param DateTime|int|string $timestamp Date string or DateTime or unix timestamp to use
356
     *
357
     * @throws \RuntimeException
358
     *
359
     * @return $this
360
     */
361 2
    public function timestamps($timestamp = null)
362
    {
363 2
        $file = $this->fPhar;
364 2
        if (!file_exists($file)) {
365 1
            $this->err(sprintf('no such file: %s', $file));
366
367 1
            return $this;
368
        }
369
370
        // operating based on UTC, squelches PHP date.timezone errors
371 1
        if (function_exists('date_default_timezone_set')) {
372 1
            date_default_timezone_set('UTC');
373
        }
374
375 1
        require_once __DIR__ . '/Timestamps.php';
376 1
        $ts = new Timestamps($file);
377 1
        $ts->updateTimestamps($timestamp);
378 1
        $ts->save($this->fPhar, Phar::SHA1);
379
380 1
        return $this;
381
    }
382
383
    /**
384
     * output information about built phar file
385
     *
386
     * @throws \RuntimeException
387
     * @throws \UnexpectedValueException
388
     * @throws \BadMethodCallException
389
     *
390
     * @return self
391
     */
392 2
    public function info()
393
    {
394 2
        $filename = $this->fPhar;
395
396 2
        if (!is_file($filename)) {
397 1
            $this->err(sprintf('no such file: %s', $filename));
398
399 1
            return $this;
400
        }
401
402 1
        printf("file.....: %s\n", $filename);
403 1
        printf("size.....: %s bytes\n", number_format(filesize($filename), 0, '.', ' '));
404 1
        printf("SHA-1....: %s\n", sha1_file($filename));
405 1
        printf("SHA-256..: %s\n", hash_file('sha256', $filename));
406
407 1
        $pinfo = new Phar($filename);
408 1
        printf("file-ver.: %s\n", $pinfo->getVersion());
409 1
        printf("api......: %s\n", $pinfo::apiVersion());
410 1
        printf("extension: %s\n", phpversion('phar'));
411 1
        printf("php......: %s\n", PHP_VERSION);
412 1
        $composer = ($buf = getenv('COMPOSER_BINARY')) ? sprintf('%s -f %s --', defined('PHP_BINARY') ? constant('PHP_BINARY') : 'php', escapeshellarg($buf)) : 'composer';
413 1
        printf("composer.: %s\n", exec($composer . ' -n --version 2>/dev/null'));
414 1
        printf("uname....: %s\n", php_uname('a'));
415 1
        printf("count....: %d file(s)\n", $pinfo->count());
416 1
        $sig = $pinfo->getSignature();
417 1
        printf("signature: %s %s\n", $sig['hash_type'], $sig['hash']);
418
419 1
        return $this;
420
    }
421
422
    /**
423
     * remove from collected files based on pattern
424
     *
425
     * @param $pattern
426
     * @param bool $error [optional] with no pattern match
427
     *
428
     * @return $this
429
     */
430 2
    public function remove($pattern, $error = true)
431
    {
432 2
        if (empty($this->files)) {
433 1
            $this->err(sprintf("can not remove from no files (pattern: '%s')", $pattern));
434
435 1
            return $this;
436
        }
437
438 2
        require_once __DIR__ . '/../../src/Glob.php';
439
440 2
        $result = array();
441 2
        foreach ($this->files as $key => $value) {
442 2
            if (!Glob::match($pattern, $key)) {
443 2
                $result[$key] = $value;
444
            }
445
        }
446
447 2
        if (count($result) === count($this->files)) {
448 1
            call_user_func(
449 1
                $error ? array($this, 'err') : array($this, 'errOut'),
450 1
                sprintf("ineffective removal pattern: '%s'", $pattern)
451
            );
452
        } else {
453 2
            $this->files = $result;
454
        }
455
456 2
        return $this;
457
    }
458
459
    /**
460
     * execute a system command
461
     *
462
     * @param string $command
463
     * @param string $return [by-ref] last line of the output (w/o newline/white space at end)
464
     * @param-out string $return
465
     *
466
     * @throws \RuntimeException
467
     *
468
     * @return $this
469
     */
470 2
    public function exec($command, &$return = null)
471
    {
472 2
        $return = exec($command, $output, $status);
473 2
        if (0 !== $status) {
474 1
            $this->err(sprintf('command failed: %s (exit status: %d)', $command, $status));
475
        }
476
477 2
        $return = rtrim($return);
478
479 2
        return $this;
480
    }
481
482
    /**
483
     * Execute a utility written in PHP w/ the current PHP binary automatically
484
     *
485
     * @param string $command
486
     * @param string $return [by-ref]  last line of the output (w/o newline/white space at end)
487
     *
488
     * @throws \RuntimeException
489
     *
490
     * @return $this
491
     *
492
     * @see Builder::exec()
493
     *
494
     */
495 2
    public function phpExec($command, &$return = null)
496
    {
497 2
        list($utility, $parameters) = preg_split('(\s)', $command, 2) + array(1 => null);
498
        /** @var string $utility */
499
500 2
        $phpUtility = sprintf(
501
            '%s -f %s --',
502 2
            escapeshellcmd(Lib::phpBinary()),
503 2
            is_file($utility) ? $utility : exec(sprintf('which %s', escapeshellarg($utility)), $blank, $status)
504
        );
505 2
        if (isset($status) && 0 !== $status) {
506 1
            $this->err(sprintf(
507
                '%s: unable to resolve "%s", verify the file exists and it is an actual php utility',
508
                'php command error',
509
                $utility
510
            ));
511
512 1
            return $this;
513
        }
514
515 1
        return $this->exec($phpUtility . ' ' . $parameters, $return);
516
    }
517
518
    /**
519
     * @return array error messages
520
     */
521 4
    public function errors()
522
    {
523 4
        return $this->errors;
524
    }
525
526
    /**
527
     * @param string $message
528
     *
529
     * @throws \RuntimeException
530
     *
531
     * @return void
532
     */
533 7
    public function err($message)
534
    {
535 7
        $this->errors[] = $message;
536 7
        $this->errOut($message);
537
    }
538
539
    /**
540
     * @param string $fphar
541
     *
542
     * @return void
543
     */
544 10
    private function _ctor($fphar)
545
    {
546 10
        $this->files = array();
547 10
        $this->errors = array();
548 10
        $this->limit(9);
549 10
        $this->fPhar = $fphar;
550
    }
551
552
    /**
553
     * add files to build the phar archive from
554
     *
555
     * @param string $pattern glob pattern of files to add
556
     * @param callable $callback [optional] callback to apply on each file found
557
     * @param string $directory [optional]
558
     * @param string $alias [optional]
559
     *
560
     * @throws \RuntimeException
561
     *
562
     * @return void
563
     */
564 4
    private function _add($pattern, $callback = null, $directory = null, $alias = null)
565
    {
566
        /** @var string $pwd [optional] previous working directory */
567 4
        $pwd = null;
568
569 4
        if (!empty($directory)) {
570
            // TODO handle errors
571 2
            $pwd = getcwd();
572 2
            chdir($directory);
573
        }
574
575 4
        $results = $this->_glob($pattern);
576 4
        foreach ($results as $result) {
577 4
            if (!is_file($result)) {
578 1
                continue;
579
            }
580
581 4
            $file = $directory . $result;
582 4
            $localName = $alias . $result;
583 4
            $descriptor = array('fil', $file);
584
585 4
            if (null !== $callback) {
586 3
                $descriptor = call_user_func($callback, $file);
587 3
                if (!is_array($descriptor) || 2 !== count($descriptor)) {
588 1
                    $this->err(sprintf(
589
                        "%s: invalid callback return for pattern '%s': %s",
590
                        $result,
591
                        $pattern,
592 1
                        rtrim(var_export($descriptor, true))
593
                    ));
594
595 1
                    continue;
596
                }
597
            }
598
599 4
            $this->files[$localName] = $descriptor;
600
        }
601
602 4
        if (!empty($directory)) {
603
            // TODO handle errors
604 2
            chdir($pwd);
605
        }
606
    }
607
608
    /**
609
     * glob with brace
610
     *
611
     * @see Builder::_glob()
612
     *
613
     * @param string $glob
614
     * @param int $flags
615
     *
616
     * @throws \RuntimeException
617
     *
618
     * @return array|false
619
     */
620 4
    private function _glob_brace($glob, $flags)
621
    {
622 4
        $reservoir = array();
623 4
        $globs = Glob::expandBrace($glob);
624 4
        foreach ($globs as $globEx) {
625 4
            $result = \glob($globEx, $flags);
626 4
            if (false === $result) {
627
                // @codeCoverageIgnoreStart
628
                $this->err(vsprintf(
629
                    "glob failure '%s' <- '%s'",
630
                    array($globEx, $glob)
631
                ));
632
633
                return false;
634
                // @codeCoverageIgnoreEnd
635
            }
636
637 4
            $result = preg_replace('(//+)', '/', $result);
638
639 4
            foreach ($result as $file) {
640 4
                $reservoir["k{$file}"] = $file;
641
            }
642
        }
643
644 4
        return array_values($reservoir);
645
    }
646
647
    /**
648
     * glob with double dot (**) support
649
     *
650
     * @param string $pattern
651
     *
652
     * @throws \UnexpectedValueException
653
     * @throws \RuntimeException
654
     *
655
     * @return array
656
     */
657 4
    private function _glob($pattern)
658
    {
659
        /* enable double-dots (with recursion limit, @see Builder::limit */
660 4
        $glob = strtr($pattern, array('\*' => '\*', '**' => '{' . $this->double . '/,}'));
661
662 4
        $result = $this->_glob_brace($glob, GLOB_NOSORT);
663
664 4
        if (false === $result) {
665
            // @codeCoverageIgnoreStart
666
            $this->err(vsprintf(
667
                "glob failure '%s' -> '%s'",
668
                array($pattern, $glob)
669
            ));
670
671
            return array();
672
            // @codeCoverageIgnoreEnd
673
        }
674 4
        if (array() === $result) {
675 1
            $this->err(sprintf(
676
                'ineffective pattern: %s',
677 1
                $pattern === $glob
678 1
                    ? $pattern
679 1
                    : sprintf("'%s' -> '%s'", $pattern, $glob)
680
            ));
681
        }
682
683 4
        return $result;
684
    }
685
686
    /**
687
     * build chunks from files (sorted by local name)
688
     *
689
     * @param array $files
690
     *
691
     * @throws \UnexpectedValueException
692
     * @throws \RuntimeException
693
     *
694
     * @return array
695
     */
696 2
    private function _bchunks(array $files)
697
    {
698 2
        ksort($files, SORT_STRING) || $this->err('internal: _bchunks ksort failed');
699
700 2
        $lastType = null;
701 2
        $chunks = array();
702 2
        $nodes = null;
703 2
        foreach ($files as $localName => $descriptor) {
704 2
            list($type, $context) = $descriptor;
705
706 2
            if ($type !== $lastType) {
707 2
                unset($nodes);
708 2
                $nodes = array();
709 2
                $chunks[] = array('type' => $type, 'nodes' => &$nodes);
710 2
                $lastType = $type;
711
            }
712
713 2
            switch ($type) {
714 2
                case 'fil': # type is: key'ed file is (existing) file with relative path on system
715 2
                    if (!is_file($context)) {
716 1
                        $this->err(sprintf('%s: not a file: %s', $localName, $context));
717
                    } else {
718 1
                        $nodes[$localName] = $context;
719
                    }
720
721 2
                    break;
722 1
                case 'str': # type is: key'ed file is string contents
723 1
                    $nodes[$localName] = $context;
724
725 1
                    break;
726
                default:
727
                    throw new \UnexpectedValueException(sprintf('unknown type: %s', $type));
728
            }
729
        }
730 2
        unset($nodes);
731
732 2
        return $chunks;
733
    }
734
735
    /**
736
     * create temporary file
737
     *
738
     * @param string $suffix [optional]
739
     *
740
     * @throws \RuntimeException
741
     *
742
     * @return false|string
743
     */
744 6
    private function _tempname($suffix = null)
745
    {
746 6
        $temp = tempnam(sys_get_temp_dir(), 'pharbuild.');
747 6
        if (false === $temp) {
748
            // @codeCoverageIgnoreStart
749
            $this->err('failed to acquire temp filename');
750
751
            return false;
752
            // @codeCoverageIgnoreEnd
753
        }
754
755 6
        if (null !== $suffix) {
756 6
            unlink($temp);
757 6
            $temp .= $suffix;
758
        }
759
760 6
        $this->unlink[$temp] = 1;
761
762 6
        return $temp;
763
    }
764
765
    /**
766
     * output message on stderr
767
     *
768
     * not counting as error, use $this->err($message) to make $message a build error
769
     *
770
     * stderr for Builder is $this->errHandle
771
     *
772
     * @param string $message
773
     *
774
     * @throws \RuntimeException
775
     *
776
     * @return void
777
     */
778 7
    private function errOut($message)
779
    {
780
        // fallback to global static: if STDIN is used for PHP
781
        // process, the default constants aren't ignored.
782 7
        if (null === $this->errHandle) {
783 5
            $this->errHandle = $this->errHandleFromEnvironment();
784
        }
785
786 7
        is_resource($this->errHandle) && fprintf($this->errHandle, "%s\n", $message);
787
    }
788
789
    /**
790
     * @throws \RuntimeException
791
     *
792
     * @return resource handle of the system's standard error stream
793
     */
794 5
    private function errHandleFromEnvironment()
795
    {
796 5
        if (defined('STDERR')) {
797
            // @codeCoverageIgnoreStart
798
            // explicit: phpunit 6 can not test this code cleanly as it is always
799
            // not defined in phpt tests due to PHP having STDERR not set when a
800
            // php file read is STDIN (which is the case for phpt tests for PHP
801
            // code) so this is a work around as this code is tested w/ phpt.
802
            $handle = constant('STDERR');
803
            /**
804
             * @psalm-suppress TypeDoesNotContainType
805
             * @psalm-suppress RedundantCondition
806
             */
807
            if (false === is_resource($handle)) {
808
                $message = 'fatal i/o error: failed to acquire stream from STDERR';
809
                $this->errors[] = $message;
810
811
                throw new \RuntimeException($message);
812
            }
813
            // @codeCoverageIgnoreEnd
814
        } else {
815
            // @codeCoverageIgnoreStart
816
            // explicit: phpunit 7.5+ can not test this code cleanly as it is
817
            // a fall-back for a previous phpunit version not having STDERR in
818
            // phpt tests available (see above)
819
            $handle = fopen('php://stderr', 'wb');
820
            if (false === $handle) {
821
                $message = 'fatal i/o error: failed to open php://stderr';
822
                $this->errors[] = $message;
823
824
                throw new \RuntimeException($message);
825
            }
826
            // @codeCoverageIgnoreEnd
827
        }
828
829 5
        return $handle;
830
    }
831
832
    /**
833
     * @param Phar $phar
834
     * @param array $files
835
     *
836
     * @throws \RuntimeException
837
     *
838
     * @return int number of files (successfully) added to the phar file
839
     */
840 2
    private function _bfiles(Phar $phar, array $files)
841
    {
842 2
        $builders = array(
843 2
            'fil' => function (array $nodes) use ($phar) {
844 2
                $result = $phar->buildFromIterator(
845 2
                    new \ArrayIterator($nodes)
846
                );
847
848 2
                return count($result);
849
            },
850 2
            'str' => function (array $nodes) use ($phar) {
851 1
                $count = 0;
852 1
                foreach ($nodes as $localName => $contents) {
853 1
                    $phar->addFromString($localName, $contents);
854 1
                    $count++;
855
                }
856
857 1
                return $count;
858
            },
859
        );
860
861 2
        $count = 0;
862 2
        foreach ($this->_bchunks($files) as $chunk) {
863 2
            $count += call_user_func($builders[$chunk['type']], $chunk['nodes']);
864
        }
865
866 2
        return $count;
867
    }
868
}
869