Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
File
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
9 / 9
29
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDir
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getDir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasDir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 setFormat
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 appendNdJsonToFile
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 clear
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
7
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\Debug\Storage;
16
17use Pop\Csv\Csv;
18use Pop\Debug\Handler\AbstractHandler;
19
20/**
21 * Debug file storage class
22 *
23 * @category   Pop
24 * @package    Pop\Debug
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    4.0.0
29 */
30class File extends AbstractStorage
31{
32
33    /**
34     * Storage dir
35     * @var ?string
36     */
37    protected ?string $dir = null;
38
39    /**
40     * Supported formats
41     * @var array
42     */
43    protected const array FORMATS = ['csv', 'tsv', 'ndjson'];
44
45    /**
46     * Format (csv, tsv or ndjson)
47     * @var string
48     */
49    protected string $format = 'csv';
50
51    /**
52     * Constructor
53     *
54     * Instantiate the file storage object
55     *
56     * @param string $dir
57     * @param string $format
58     */
59    public function __construct(string $dir, string $format = 'csv')
60    {
61        $this->setDir($dir);
62        $this->setFormat($format);
63    }
64
65    /**
66     * Set the current storage dir
67     *
68     * @param  string $dir
69     * @throws Exception
70     * @return File
71     */
72    public function setDir(string $dir): File
73    {
74        if (!file_exists($dir)) {
75            throw new Exception('Error: That directory does not exist.');
76        } else if (!is_writable($dir)) {
77            throw new Exception('Error: That directory is not writable.');
78        }
79
80        $this->dir = realpath($dir);
81
82        return $this;
83    }
84
85    /**
86     * Get the storage dir
87     *
88     * @return ?string
89     */
90    public function getDir(): ?string
91    {
92        return $this->dir;
93    }
94
95    /**
96     * Has storage dir
97     *
98     * @return bool
99     */
100    public function hasDir(): bool
101    {
102        return (!empty($this->dir) && file_exists($this->dir));
103    }
104
105    /**
106     * Set the format
107     *
108     * @param  string $format
109     * @throws \InvalidArgumentException
110     * @return File
111     */
112    public function setFormat(string $format): File
113    {
114        $format = strtolower($format);
115
116        if (!in_array($format, self::FORMATS)) {
117            throw new \InvalidArgumentException('Error: The format must be "csv", "tsv" or "ndjson".');
118        }
119
120        $this->format = $format;
121
122        return $this;
123    }
124
125    /**
126     * Get the format
127     *
128     * @return string
129     */
130    public function getFormat(): string
131    {
132        return $this->format;
133    }
134
135    /**
136     * Save debug data
137     *
138     * @param  string          $id
139     * @param  string          $name
140     * @param  AbstractHandler $handler
141     * @return void
142     */
143    public function save(string $id, string $name, AbstractHandler $handler): void
144    {
145
146        $events   = $this->prepareEvents($id, $name, $handler);
147        $filename = $this->dir . DIRECTORY_SEPARATOR . $id . '-' . $name . '.' . $this->format;
148
149        if ($this->format === 'ndjson') {
150            $this->appendNdJsonToFile($filename, $events);
151            return;
152        }
153
154        if (!file_exists($filename) && isset($events[0])) {
155            file_put_contents($filename, Csv::getFieldHeaders($events[0], (($this->format == 'tsv') ? "\t" : ',')));
156        }
157
158        Csv::appendDataToFile($filename, $events, ['delimiter' => (($this->format == 'tsv') ? "\t" : ',')]);
159    }
160
161    /**
162     * Append events to an NDJSON (JSON Lines) file, one self-contained JSON object per line
163     *
164     * The 'context' field of each event is already a json_encode()'d string (see
165     * AbstractStorage::prepareEvents(), shared with the CSV/TSV formats, where it needs to be a
166     * flat scalar cell). For NDJSON it's decoded back into a real nested value first, so it comes
167     * out as proper nested JSON instead of a doubly-escaped string.
168     *
169     * @param  string $filename
170     * @param  array  $events
171     * @return void
172     */
173    protected function appendNdJsonToFile(string $filename, array $events): void
174    {
175        $lines = '';
176
177        foreach ($events as $event) {
178            if (isset($event['context']) && is_string($event['context'])) {
179                $context = json_decode($event['context'], true);
180                if (json_last_error() === JSON_ERROR_NONE) {
181                    $event['context'] = $context;
182                }
183            }
184
185            $encoded = json_encode($event, JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR);
186            $lines  .= (($encoded !== false) ? $encoded : '{}') . PHP_EOL;
187        }
188
189        file_put_contents($filename, $lines, FILE_APPEND);
190    }
191
192    /**
193     * Clear all debug data
194     *
195     * @return void
196     */
197    public function clear(): void
198    {
199        if (!$dh = @opendir($this->dir)) {
200            return;
201        }
202
203        while (false !== ($obj = readdir($dh))) {
204            if (($obj != '.') && ($obj != '..') &&
205                !is_dir($this->dir . DIRECTORY_SEPARATOR . $obj) && is_file($this->dir . DIRECTORY_SEPARATOR . $obj)) {
206                unlink($this->dir . DIRECTORY_SEPARATOR . $obj);
207            }
208        }
209
210        closedir($dh);
211    }
212
213}