Passed
Push — master ( 88d9d6...ec4c8d )
by Tom
04:42
created

Builder::build()   C

Complexity

Conditions 12
Paths 86

Size

Total Lines 71
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 35
c 1
b 0
f 0
nc 86
nop 1
dl 0
loc 71
ccs 33
cts 33
cp 1
crap 12
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) do 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 3
    }
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 1
                    '%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
    public function snapShot()
178
    {
179 1
        return function ($file) {
180 1
            $source = fopen($file, 'rb');
181 1
            if (false === $source) {
182 1
                $this->err(sprintf('failed to open for reading: %s', $file));
183
184 1
                return null;
185
            }
186
187 1
            $target = tmpfile();
188 1
            if (false === $target) {
189
                // @codeCoverageIgnoreStart
190
                fclose($source);
191
                $this->err(sprintf('failed to open temp file for writing'));
192
193
                return null;
194
                // @codeCoverageIgnoreEnd
195
            }
196
197 1
            $meta = stream_get_meta_data($target);
198 1
            $snapShotFile = $meta['uri'];
199
200 1
            if (false === (bool)stream_copy_to_stream($source, $target)) {
201
                // @codeCoverageIgnoreStart
202
                $this->err(sprintf('stream copy error: %s', $file));
203
                fclose($source);
204
                fclose($target);
205
                unlink($snapShotFile);
206
207
                return null;
208
                // @codeCoverageIgnoreEnd
209
            }
210 1
            fclose($source);
211
212
            # preserve file from deletion until later cleanup
213 1
            $this->unlink[$snapShotFile] = $target;
214
215 1
            return array('fil', $snapShotFile);
216 1
        };
217
    }
218
219
    /**
220
     * Drop first line from file when added to the build, e.g.
221
     * for removing a shebang line.
222
     *
223
     * @throws \RuntimeException
224
     *
225
     * @return \Closure
226
     */
227
    public function dropFirstLine()
228
    {
229 3
        return function ($file) {
230 3
            $lines = file($file);
231 3
            if (false === $lines) {
232 1
                $this->err(sprintf('error reading file: %s', $file));
233
234 1
                return null;
235
            }
236 2
            array_shift($lines);
237 2
            $buffer = implode('', $lines);
238
239 2
            return array('str', $buffer);
240 3
        };
241
    }
242
243
    /**
244
     * String replace on file contents
245
     *
246
     * @param string $that
247
     * @param string $with
248
     *
249
     * @return \Closure
250
     */
251
    public function replace($that, $with)
252
    {
253 1
        return function ($file) use ($that, $with) {
254 1
            $buffer = file_get_contents($file);
255 1
            $buffer = strtr($buffer, array($that => $with));
256
257 1
            return array('str', $buffer);
258 1
        };
259
    }
260
261
    /**
262
     * build phar file and optionally invoke it with parameters for
263
     * a quick smoke test
264
     *
265
     * @param string $params [options]
266
     *
267
     * @throws \RuntimeException
268
     * @throws \UnexpectedValueException
269
     * @throws \BadMethodCallException
270
     *
271
     * @return $this
272
     */
273 6
    public function build($params = null)
274
    {
275 6
        $file = $this->fPhar;
276 6
        $files = $this->files;
277
278 6
        $temp = $this->_tempname('.phar');
279 6
        if (false === $temp) {
280
            // @codeCoverageIgnoreStart
281
            $this->err('fatal: failed to create tmp phar archive file');
282
283
            return $this;
284
            // @codeCoverageIgnoreEnd
285
        }
286
287 6
        if (file_exists($file) && !unlink($file)) {
288 1
            $this->err(sprintf("could not unlink existing file '%s'", $file));
289
290 1
            return $this;
291
        }
292
293 5
        if (!Phar::canWrite()) {
294 1
            $this->err("phar: writing phar files is disabled by the php.ini setting 'phar.readonly'");
295
        }
296
297 5
        if (empty($files)) {
298 1
            $this->err('no files, add some or do not remove all');
299
        }
300
301 5
        if (!empty($this->errors)) {
302 3
            $this->err('fatal: build has errors, not building');
303
304 3
            return $this;
305
        }
306
307 2
        $phar = new Phar($temp);
308 2
        $phar->startBuffering();
309
310 2
        if (null !== $this->stub) {
311 1
            $phar->setStub($this->stub);
312
        }
313
314 2
        $count = $this->_bfiles($phar, $files);
315 2
        if (count($files) !== $count) {
316 1
            $this->err(sprintf('only %d of %d files could be added', $count, count($files)));
317
        }
318
319 2
        $phar->stopBuffering();
320 2
        unset($phar); # save file
321
322 2
        if (0 === $count) {
323 1
            $this->err('fatal: no files in phar archive, must have at least one');
324
325 1
            return $this;
326
        }
327
328 1
        copy($temp, $file);
329
330
        # chmod +x for lazy ones
331 1
        if (!chmod($file, 0775)) {
332
            // @codeCoverageIgnoreStart
333
            $this->err('error changing mode to 0775 on phar file');
334
            // @codeCoverageIgnoreEnd
335
        }
336
337
        # smoke test TODO operate on secondary temp file, execution options
338 1
        if (null !== $params) {
339 1
            $this->exec(sprintf('./%s %s', $file, $params), $return);
340 1
            printf("%s\n", $return);
341
        }
342
343 1
        return $this;
344
    }
345
346
    /**
347
     * updates each file's unix timestamps in the phar archive,
348
     * useful for reproducible builds
349
     *
350
     * @param DateTime|int|string $timestamp Date string or DateTime or unix timestamp to use
351
     *
352
     * @throws \RuntimeException
353
     *
354
     * @return $this
355
     */
356 2
    public function timestamps($timestamp = null)
357
    {
358 2
        $file = $this->fPhar;
359 2
        if (!file_exists($file)) {
360 1
            $this->err(sprintf('no such file: %s', $file));
361
362 1
            return $this;
363
        }
364 1
        require_once __DIR__ . '/Timestamps.php';
365 1
        $ts = new Timestamps($file);
366 1
        $ts->updateTimestamps($timestamp);
367 1
        $ts->save($this->fPhar, Phar::SHA1);
368
369 1
        return $this;
370
    }
371
372
    /**
373
     * output information about built phar file
374
     *
375
     * @throws \RuntimeException
376
     * @throws \UnexpectedValueException
377
     * @throws \BadMethodCallException
378
     *
379
     * @return self
380
     */
381 2
    public function info()
382
    {
383 2
        $filename = $this->fPhar;
384
385 2
        if (!is_file($filename)) {
386 1
            $this->err(sprintf('no such file: %s', $filename));
387
388 1
            return $this;
389
        }
390
391 1
        printf("file.....: %s\n", $filename);
392 1
        printf("size.....: %s bytes\n", number_format(filesize($filename), 0, '.', ' '));
393 1
        printf("SHA-1....: %s\n", strtoupper(sha1_file($filename)));
394 1
        printf("SHA-256..: %s\n", strtoupper(hash_file('sha256', $filename)));
395
396 1
        $pinfo = new Phar($filename);
397 1
        printf("file.....: %s\n", $pinfo->getVersion());
398 1
        printf("api......: %s\n", $pinfo::apiVersion());
399 1
        printf("extension: %s\n", phpversion('phar'));
400 1
        printf("php......: %s\n", PHP_VERSION);
401 1
        printf("composer.: %s\n", exec('composer -n --version 2>/dev/null'));
402 1
        printf("uname....: %s\n", php_uname('a'));
403 1
        printf("count....: %d file(s)\n", $pinfo->count());
404 1
        $sig = $pinfo->getSignature();
405 1
        printf("signature: %s %s\n", $sig['hash_type'], $sig['hash']);
406
407 1
        return $this;
408
    }
409
410
    /**
411
     * remove from collected files based on pattern
412
     *
413
     * @param $pattern
414
     *
415
     * @throws \RuntimeException
416
     *
417
     * @return $this
418
     */
419 2
    public function remove($pattern)
420
    {
421 2
        if (empty($this->files)) {
422 1
            $this->err(sprintf("can not remove from no files (pattern: '%s')", $pattern));
423
424 1
            return $this;
425
        }
426
427 2
        require_once __DIR__ . '/../../src/Glob.php';
428
429 2
        $result = array();
430 2
        foreach ($this->files as $key => $value) {
431 2
            if (!Glob::match($pattern, $key)) {
432 2
                $result[$key] = $value;
433
            }
434
        }
435
436 2
        if (count($result) === count($this->files)) {
437 1
            $this->err(sprintf("ineffective removal pattern: '%s'", $pattern));
438
        } else {
439 2
            $this->files = $result;
440
        }
441
442 2
        return $this;
443
    }
444
445
    /**
446
     * execute a system command
447
     *
448
     * @param string $command
449
     * @param string $return [by-ref] last line of the output (w/o newline/white space at end)
450
     * @param-out string $return
451
     *
452
     * @throws \RuntimeException
453
     *
454
     * @return $this
455
     */
456 2
    public function exec($command, &$return = null)
457
    {
458 2
        $return = exec($command, $output, $status);
459 2
        if (0 !== $status) {
460 1
            $this->err(sprintf('command failed: %s (exit status: %d)', $command, $status));
461
        }
462
463 2
        $return = rtrim($return);
464
465 2
        return $this;
466
    }
467
468
    /**
469
     * Execute a utility written in PHP w/ the current PHP binary automatically
470
     *
471
     * @param string $command
472
     * @param string $return [by-ref]  last line of the output (w/o newline/white space at end)
473
     *
474
     * @throws \RuntimeException
475
     *
476
     * @return $this
477
     *
478
     * @see Builder::exec()
479
     *
480
     */
481 2
    public function phpExec($command, &$return = null)
482
    {
483 2
        list($utility, $parameters) = preg_split('(\s)', $command, 2) + array(1 => null);
484
        /** @var string $utility */
485
486 2
        $phpUtility = sprintf(
487 2
            '%s -f %s --',
488 2
            escapeshellcmd(Lib::phpBinary()),
489 2
            is_file($utility) ? $utility : exec(sprintf('which %s', escapeshellarg($utility)), $blank, $status)
490
        );
491 2
        if (isset($status) && 0 !== $status) {
492 1
            $this->err(sprintf(
493 1
                '%s: unable to resolve "%s", verify the file exists and it is an actual php utility',
494 1
                'php command error',
495
                $utility
496
            ));
497
498 1
            return $this;
499
        }
500
501 1
        return $this->exec($phpUtility . ' ' . $parameters, $return);
502
    }
503
504
    /**
505
     * @return array error messages
506
     */
507 4
    public function errors()
508
    {
509 4
        return $this->errors;
510
    }
511
512
    /**
513
     * @param string $fphar
514
     *
515
     * @return void
516
     */
517 10
    private function _ctor($fphar)
518
    {
519 10
        $this->files = array();
520 10
        $this->errors = array();
521 10
        $this->limit(9);
522 10
        $this->fPhar = $fphar;
523 10
    }
524
525
    /**
526
     * add files to build the phar archive from
527
     *
528
     * @param string $pattern glob pattern of files to add
529
     * @param callable $callback [optional] callback to apply on each file found
530
     * @param string $directory [optional]
531
     * @param string $alias [optional]
532
     *
533
     * @throws \RuntimeException
534
     *
535
     * @return void
536
     */
537 4
    private function _add($pattern, $callback = null, $directory = null, $alias = null)
538
    {
539
        /** @var string $pwd [optional] previous working directory */
540 4
        $pwd = null;
541
542 4
        if (!empty($directory)) {
543
            // TODO handle errors
544 2
            $pwd = getcwd();
545 2
            chdir($directory);
546
        }
547
548 4
        $results = $this->_glob($pattern);
549 4
        foreach ($results as $result) {
550 4
            if (!is_file($result)) {
551 1
                continue;
552
            }
553
554 4
            $file = $directory . $result;
555 4
            $localName = $alias . $result;
556 4
            $descriptor = array('fil', $file);
557
558 4
            if (null !== $callback) {
559 3
                $descriptor = call_user_func($callback, $file);
560 3
                if (!is_array($descriptor) || 2 !== count($descriptor)) {
561 1
                    $this->err(sprintf(
562 1
                        "%s: invalid callback return for pattern '%s': %s",
563
                        $result,
564
                        $pattern,
565 1
                        rtrim(var_export($descriptor, true))
566
                    ));
567
568 1
                    continue;
569
                }
570
            }
571
572 4
            $this->files[$localName] = $descriptor;
573
        }
574
575 4
        if (!empty($directory)) {
576
            // TODO handle errors
577 2
            chdir($pwd);
578
        }
579 4
    }
580
581
    /**
582
     * glob with brace
583
     *
584
     * @see Builder::_glob()
585
     *
586
     * @param string $glob
587
     * @param int $flags
588
     *
589
     * @throws \RuntimeException
590
     *
591
     * @return array|false
592
     */
593 4
    private function _glob_brace($glob, $flags)
594
    {
595 4
        $reservoir = array();
596 4
        $globs = Glob::expandBrace($glob);
597 4
        foreach ($globs as $globEx) {
598 4
            $result = \glob($globEx, $flags);
599 4
            if (false === $result) {
600
                // @codeCoverageIgnoreStart
601
                $this->err(vsprintf(
602
                    "glob failure '%s' <- '%s'",
603
                    array($globEx, $glob)
604
                ));
605
606
                return false;
607
                // @codeCoverageIgnoreEnd
608
            }
609
610 4
            $result = preg_replace('(//+)', '/', $result);
611
612 4
            foreach ($result as $file) {
613 4
                $reservoir["k{$file}"] = $file;
614
            }
615
        }
616
617 4
        return array_values($reservoir);
618
    }
619
620
    /**
621
     * glob with double dot (**) support
622
     *
623
     * @param string $pattern
624
     *
625
     * @throws \UnexpectedValueException
626
     * @throws \RuntimeException
627
     *
628
     * @return array
629
     */
630 4
    private function _glob($pattern)
631
    {
632
        /* enable double-dots (with recursion limit, @see Builder::limit */
633 4
        $glob = strtr($pattern, array('\*' => '\*', '**' => '{' . $this->double . '/,}'));
634
635 4
        $result = $this->_glob_brace($glob, GLOB_NOSORT);
636
637 4
        if (false === $result) {
638
            // @codeCoverageIgnoreStart
639
            $this->err(vsprintf(
640
                "glob failure '%s' -> '%s'",
641
                array($pattern, $glob)
642
            ));
643
644
            return array();
645
            // @codeCoverageIgnoreEnd
646
        }
647 4
        if (array() === $result) {
648 1
            $this->err(sprintf(
649 1
                'ineffective pattern: %s',
650 1
                $pattern === $glob
651 1
                    ? $pattern
652 1
                    : sprintf("'%s' -> '%s'", $pattern, $glob)
653
            ));
654
        }
655
656 4
        return $result;
657
    }
658
659
    /**
660
     * build chunks from files (sorted by local name)
661
     *
662
     * @param array $files
663
     *
664
     * @throws \UnexpectedValueException
665
     * @throws \RuntimeException
666
     *
667
     * @return array
668
     */
669 2
    private function _bchunks(array $files)
670
    {
671 2
        ksort($files, SORT_STRING) || $this->err('internal: _bchunks ksort failed');
672
673 2
        $lastType = null;
674 2
        $chunks = array();
675 2
        $nodes = null;
676 2
        foreach ($files as $localName => $descriptor) {
677 2
            list($type, $context) = $descriptor;
678
679 2
            if ($type !== $lastType) {
680 2
                unset($nodes);
681 2
                $nodes = array();
682 2
                $chunks[] = array('type' => $type, 'nodes' => &$nodes);
683 2
                $lastType = $type;
684
            }
685
686 2
            switch ($type) {
687 2
                case 'fil': # type is: key'ed file is (existing) file with relative path on system
688 2
                    if (!is_file($context)) {
689 1
                        $this->err(sprintf('%s: not a file: %s', $localName, $context));
690
                    } else {
691 1
                        $nodes[$localName] = $context;
692
                    }
693
694 2
                    break;
695 1
                case 'str': # type is: key'ed file is string contents
696 1
                    $nodes[$localName] = $context;
697
698 1
                    break;
699
                default:
700
                    throw new \UnexpectedValueException(sprintf('unknown type: %s', $type));
701
            }
702
        }
703 2
        unset($nodes);
704
705 2
        return $chunks;
706
    }
707
708
    /**
709
     * create temporary file
710
     *
711
     * @param string $suffix [optional]
712
     *
713
     * @throws \RuntimeException
714
     *
715
     * @return false|string
716
     */
717 6
    private function _tempname($suffix = null)
718
    {
719 6
        $temp = tempnam(sys_get_temp_dir(), 'pharbuild.');
720 6
        if (false === $temp) {
721
            // @codeCoverageIgnoreStart
722
            $this->err('failed to acquire temp filename');
723
724
            return false;
725
            // @codeCoverageIgnoreEnd
726
        }
727
728 6
        if (null !== $suffix) {
729 6
            unlink($temp);
730 6
            $temp .= $suffix;
731
        }
732
733 6
        $this->unlink[$temp] = 1;
734
735 6
        return $temp;
736
    }
737
738
    /**
739
     * @param string $message
740
     *
741
     * @throws \RuntimeException
742
     *
743
     * @return void
744
     */
745 7
    private function err($message)
746
    {
747
        // fallback to global static: if STDIN is used for PHP
748
        // process, the default constants aren't ignored.
749 7
        if (null === $this->errHandle) {
750 5
            $this->errHandle = $this->errHandleFromEnvironment();
751
        }
752
753 7
        $this->errors[] = $message;
754 7
        is_resource($this->errHandle) && fprintf($this->errHandle, "%s\n", $message);
755 7
    }
756
757
    /**
758
     * @throws \RuntimeException
759
     *
760
     * @return resource handle of the system's standard error stream
761
     */
762 5
    private function errHandleFromEnvironment()
763
    {
764 5
        if (defined('STDERR')) {
765
            // @codeCoverageIgnoreStart
766
            // explicit: phpunit 6 can not test this code cleanly as it is always
767
            // not defined in phpt tests due to PHP having STDERR not set when a
768
            // php file read is STDIN (which is the case for phpt tests for PHP
769
            // code) so this is a work around as this code is tested w/ phpt.
770
            $handle = constant('STDERR');
771
            /**
772
             * @psalm-suppress TypeDoesNotContainType
773
             * @psalm-suppress RedundantCondition
774
             */
775
            if (false === is_resource($handle)) {
776
                $message = 'fatal i/o error: failed to acquire stream from STDERR';
777
                $this->errors[] = $message;
778
779
                throw new \RuntimeException($message);
780
            }
781
            // @codeCoverageIgnoreEnd
782
        } else {
783
            // @codeCoverageIgnoreStart
784
            // explicit: phpunit 7.5+ can not test this code cleanly as it is
785
            // a fall-back for a previous phpunit version not having STDERR in
786
            // phpt tests available (see above)
787
            $handle = fopen('php://stderr', 'wb');
788
            if (false === $handle) {
789
                $message = 'fatal i/o error: failed to open php://stderr';
790
                $this->errors[] = $message;
791
792
                throw new \RuntimeException($message);
793
            }
794
            // @codeCoverageIgnoreEnd
795
        }
796
797 5
        return $handle;
798
    }
799
800
    /**
801
     * @param Phar $phar
802
     * @param array $files
803
     *
804
     * @throws \RuntimeException
805
     *
806
     * @return int number of files (successfully) added to the phar file
807
     */
808
    private function _bfiles(Phar $phar, array $files)
809
    {
810
        $builders = array(
811 2
            'fil' => function (array $nodes) use ($phar) {
812 2
                $result = $phar->buildFromIterator(
813 2
                    new \ArrayIterator($nodes)
814
                );
815
816 2
                return count($result);
817 2
            },
818 2
            'str' => function (array $nodes) use ($phar) {
819 1
                $count = 0;
820 1
                foreach ($nodes as $localName => $contents) {
821 1
                    $phar->addFromString($localName, $contents);
822 1
                    $count++;
823
                }
824
825 1
                return $count;
826 2
            },
827
        );
828
829 2
        $count = 0;
830 2
        foreach ($this->_bchunks($files) as $chunk) {
831 2
            $count += call_user_func($builders[$chunk['type']], $chunk['nodes']);
832
        }
833
834 2
        return $count;
835
    }
836
}
837