Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
Generator
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
18 / 18
53
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 addCodeObjects
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 addCodeObject
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 hasCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 code
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCloseTag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasCloseTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEnv
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getEnv
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasEnv
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFilename
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasFilename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
19
 writeToFile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 outputToHttp
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Code;
16
17use Pop\Code\Generator\GeneratorInterface;
18use Pop\Code\Generator\Traits;
19
20/**
21 * Generator code class
22 *
23 * @category   Pop
24 * @package    Pop\Code
25 * @author     Nick Sagona, III <dev@noladev.com>
26 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.0.0
29 */
30class Generator extends Generator\AbstractGenerator
31{
32
33    use Traits\DocblockTrait;
34
35    /**
36     * Code generator objects
37     * @var array
38     */
39    protected array $code = [];
40
41    /**
42     * Namespaces for the code generator objects
43     * @var array
44     */
45    protected array $namespaces = [];
46
47    /**
48     * Environment setting, i.e. #!/usr/bin/php
49     * @var ?string
50     */
51    protected ?string $env = null;
52
53    /**
54     * Flag to close the code file with ?>
55     * @var bool
56     */
57    protected bool $close = false;
58
59    /**
60     * Code filename
61     * @var ?string
62     */
63    protected ?string $filename = null;
64
65    /**
66     * Constructor
67     *
68     * Instantiate the code generator object
69     *
70     * @param  mixed $code
71     * @throws Exception
72     */
73    public function __construct(mixed $code = null)
74    {
75        if ($code !== null) {
76            if (is_array($code)) {
77                $this->addCodeObjects($code);
78            } else if ($code instanceof GeneratorInterface) {
79                $this->addCodeObject($code);
80            } else {
81                throw new Exception('Error: The code parameter was not the correct type.');
82            }
83        }
84    }
85
86    /**
87     * Add code generator objects
88     *
89     * @param  array $codeObjects
90     * @return Generator
91     */
92    public function addCodeObjects(array $codeObjects): Generator
93    {
94        foreach ($codeObjects as $namespace => $codeObject) {
95            if (!is_numeric($namespace)) {
96                if (is_array($codeObject)) {
97                    foreach ($codeObject as $code) {
98                        $this->addCodeObject($code, $namespace);
99                    }
100                } else {
101                    $this->addCodeObject($codeObject, $namespace);
102                }
103            } else {
104                $this->addCodeObject($codeObject);
105            }
106        }
107        return $this;
108    }
109
110    /**
111     * Add a code generator object
112     *
113     * @param  Generator\GeneratorInterface $codeObject
114     * @param  ?string                       $namespace
115     * @return Generator
116     */
117    public function addCodeObject(Generator\GeneratorInterface $codeObject, ?string $namespace = null): Generator
118    {
119        $this->code[] = $codeObject;
120
121        if ($namespace !== null) {
122            $key = count($this->code) - 1;
123            $this->namespaces[$key] = $namespace;
124        }
125        return $this;
126    }
127
128    /**
129     * Has code generator objects
130     *
131     * @return bool
132     */
133    public function hasCode(): bool
134    {
135        return (!empty($this->code));
136    }
137
138    /**
139     * Access the code generator object
140     *
141     * @return array
142     */
143    public function getCode(): array
144    {
145        return $this->code;
146    }
147
148    /**
149     * Access the code generator objects (alias method)
150     *
151     * @return array
152     */
153    public function code(): array
154    {
155        return $this->code;
156    }
157
158    /**
159     * Set the code close tag flag
160     *
161     * @param  bool $close
162     * @return Generator
163     */
164    public function setCloseTag(bool $close = false): Generator
165    {
166        $this->close = $close;
167        return $this;
168    }
169
170    /**
171     * Determine if the code close tag flag is set
172     *
173     * @return bool
174     */
175    public function hasCloseTag(): bool
176    {
177        return $this->close;
178    }
179
180    /**
181     * Set the environment
182     *
183     * @param  ?string $env
184     * @return Generator
185     */
186    public function setEnv(?string $env = null): Generator
187    {
188        $this->env = $env;
189        return $this;
190    }
191
192    /**
193     * Get the environment
194     *
195     * @return string|null
196     */
197    public function getEnv(): string|null
198    {
199        return $this->env;
200    }
201
202    /**
203     * Determine if the environment is set
204     *
205     * @return bool
206     */
207    public function hasEnv(): bool
208    {
209        return ($this->env !== null);
210    }
211
212    /**
213     * Set the filename
214     *
215     * @param  string $filename
216     * @return Generator
217     */
218    public function setFilename(string $filename): Generator
219    {
220        $this->filename = $filename;
221        return $this;
222    }
223
224    /**
225     * Has filename
226     *
227     * @return bool
228     */
229    public function hasFilename(): bool
230    {
231        return ($this->filename !== null);
232    }
233
234    /**
235     * Get filename
236     *
237     * @return string|null
238     */
239    public function getFilename(): string|null
240    {
241        return $this->filename;
242    }
243
244    /**
245     * Render method
246     *
247     * @return string
248     */
249    public function render(): string
250    {
251        $this->output = '';
252
253        if ($this->env !== null) {
254            $this->output .= $this->env . PHP_EOL;
255        }
256
257        $this->output    .= '<?php' . PHP_EOL;
258        $this->output    .= ($this->docblock !== null) ? $this->docblock->render() . PHP_EOL : null;
259        $currentNamespace = null;
260        $inNamespace      = false;
261
262        foreach ($this->code as $key => $code) {
263            if (isset($this->namespaces[$key]) && ($currentNamespace != $this->namespaces[$key])) {
264                if ($currentNamespace !== null) {
265                    $this->output .= '}' . PHP_EOL . PHP_EOL;
266                }
267                $namespace        = ($this->namespaces[$key] != '*') ? $this->namespaces[$key] . ' ' : null;
268                $this->output    .= 'namespace ' . $namespace . '{' . PHP_EOL;
269                $currentNamespace = $this->namespaces[$key];
270                $inNamespace      = true;
271            } else if (!isset($this->namespaces[$key]) && ($currentNamespace !== null)) {
272                $this->output .= '}' . PHP_EOL . PHP_EOL;
273                $inNamespace   = false;
274            }
275
276            // Temporarily bump the code object's own indent (and its docblock's, and its body's,
277            // if it has one) to account for nesting inside a namespace block, then restore the
278            // original values immediately after rendering -- render() must have no side effects
279            // beyond building $this->output, or calling it twice (e.g. `echo $generator;` then
280            // `$generator->writeToFile(...)`) would compound the indent further on each call.
281            $usesBodyTrait          = in_array('Pop\Code\Generator\Traits\BodyTrait', class_uses($code));
282            $hasDocblock            = $code->hasDocblock();
283            $originalDocblockIndent = null;
284            if ($currentNamespace !== null) {
285                $originalIndent = $code->getIndent();
286                $code->setIndent($originalIndent + $this->indent);
287                if ($hasDocblock) {
288                    $originalDocblockIndent = $code->getDocblock()->getIndent();
289                    $code->getDocblock()->setIndent($originalDocblockIndent + $this->indent);
290                }
291                if ($usesBodyTrait) {
292                    $code->indentBody($this->indent);
293                }
294            }
295
296            $this->output .= $code . PHP_EOL;
297
298            if ($currentNamespace !== null) {
299                $code->setIndent($originalIndent);
300                if ($hasDocblock && $originalDocblockIndent !== null) {
301                    $code->getDocblock()->setIndent($originalDocblockIndent);
302                }
303                if ($usesBodyTrait) {
304                    $code->indentBody(-$this->indent);
305                }
306            }
307        }
308
309        if ($inNamespace) {
310            $this->output .= '}' . PHP_EOL . PHP_EOL;
311        }
312
313        if ($this->close) {
314            $this->output .= '?>' . PHP_EOL;
315        }
316
317        return $this->output;
318    }
319
320    /**
321     * Write to file
322     *
323     * @param  ?string $filename
324     * @throws Exception
325     * @return void
326     */
327    public function writeToFile(?string $filename = null): void
328    {
329        if (($this->filename !== null) && ($filename === null)) {
330            $filename = $this->filename;
331        }
332        if (empty($filename)) {
333            throw new Exception('Error: The filename has not been set.');
334        }
335
336        file_put_contents($filename, $this->render());
337    }
338
339    /**
340     * Output to HTTP
341     *
342     * @param  ?string $filename
343     * @param  bool    $forceDownload
344     * @param  array   $headers
345     * @return void
346     */
347    public function outputToHttp(?string $filename = null, bool $forceDownload = false, array $headers = []): void
348    {
349        if (($this->filename !== null) && ($filename === null)) {
350            $filename = $this->filename;
351        }
352        if (empty($filename)) {
353            $filename = 'code.php';
354        }
355
356        $headers['Content-Type']        = 'text/plain';
357        $headers['Content-Disposition'] = (($forceDownload) ? 'attachment; ' : null) . 'filename=' . $filename;
358
359        // Send the headers and output the PDF
360        if (!headers_sent()) {
361            header('HTTP/1.1 200 OK');
362            foreach ($headers as $name => $value) {
363                header($name . ': ' . $value);
364            }
365        }
366
367        echo $this->render();
368    }
369
370    /**
371     * Print code
372     *
373     * @return string
374     */
375    public function __toString(): string
376    {
377        return $this->render();
378    }
379
380}