Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
File
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
14 / 14
45
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getFolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileTimestamp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 filterStatesByTimestamp
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
9
 decode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 send
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getStates
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 getStateById
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getStateByModel
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getStateByTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStateByDate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getSnapshot
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
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 <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Audit\Adapter;
16
17/**
18 * Auditor file class
19 *
20 * @category   Pop
21 * @package    Pop\Audit
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    3.0.0
26 */
27class File extends AbstractAdapter
28{
29
30    /**
31     * Folder to store the audit results
32     * @var ?string
33     */
34    protected ?string $folder = null;
35
36
37    /**
38     * File prefix
39     * @var string
40     */
41    protected string $prefix = 'pop-audit-';
42
43    /**
44     * Constructor
45     *
46     * Instantiate the file adapter object
47     *
48     * @param  string $folder
49     * @param  string $prefix
50     * @throws Exception
51     */
52    public function __construct(string $folder, string $prefix = 'pop-audit-')
53    {
54        if (!file_exists($folder)) {
55            throw new Exception('That folder does not exist.');
56        }
57        $this->folder = $folder;
58        $this->prefix = $prefix;
59    }
60
61    /**
62     * Get the folder
63     *
64     * @return string
65     */
66    public function getFolder(): string
67    {
68        return $this->folder;
69    }
70
71    /**
72     * Get the prefix
73     *
74     * @return string
75     */
76    public function getPrefix(): string
77    {
78        return $this->prefix;
79    }
80
81    /**
82     * Get ID from filename
83     *
84     * @param  string $filename
85     * @return string
86     */
87    public function getId(string $filename): string
88    {
89        $filename = substr($filename, 0, strrpos($filename, '.'));
90        $filename = substr($filename, 0, strrpos($filename, '-'));
91        return substr($filename, (strrpos($filename, '-') + 1));
92    }
93
94    /**
95     * Get the timestamp embedded in the filename, written by send()
96     *
97     * @param  string $filename
98     * @return int
99     */
100    protected function getFileTimestamp(string $filename): int
101    {
102        $name = substr($filename, 0, strrpos($filename, '.'));
103        return (int)substr($name, strrpos($name, '-') + 1);
104    }
105
106    /**
107     * Get model states filtered by a timestamp range
108     *
109     * @param  int  $from
110     * @param  ?int $backTo
111     * @return array
112     */
113    protected function filterStatesByTimestamp(int $from, ?int $backTo = null): array
114    {
115        $files   = scandir($this->folder);
116        $results = [];
117
118        foreach ($files as $file) {
119            if (($file != '.') && ($file != '..')) {
120                $mtime = $this->getFileTimestamp($file);
121                if ((($backTo !== null) && ($mtime <= $from) && ($mtime >= $backTo)) ||
122                    (($backTo === null) && ($mtime <= $from))) {
123                    $results[$file] = $this->decode($file);
124                }
125            }
126        }
127
128        return $results;
129    }
130
131    /**
132     * Decode the audit file
133     *
134     * @param  string $filename
135     * @throws Exception
136     * @return array|false|null
137     */
138    public function decode(string $filename): array|false|null
139    {
140        if (!file_exists($this->folder . DIRECTORY_SEPARATOR . $filename)) {
141            throw new Exception('That audit file does not exist.');
142        }
143
144        return json_decode(file_get_contents($this->folder . DIRECTORY_SEPARATOR . $filename), true);
145    }
146
147    /**
148     * Send the results of the audit
149     *
150     * @throws Exception
151     * @return string
152     */
153    public function send(): string
154    {
155        if ($this->action === null) {
156            throw new Exception('The model state differences have not been resolved.');
157        }
158        if (($this->model === null) || ($this->modelId === null)) {
159            throw new Exception('The model has not been set.');
160        }
161
162        $id       = md5($this->model . '-' . $this->modelId) . '-' .uniqid() . '-' . time();
163        $filename = $this->prefix . $id . '.log';
164        file_put_contents(
165            $this->folder . DIRECTORY_SEPARATOR . $filename,
166            json_encode($this->prepareData(false), JSON_PRETTY_PRINT)
167        );
168
169        return $filename;
170    }
171
172    /**
173     * Get model states
174     *
175     * @param  string $sort
176     * @param  ?int   $limit
177     * @param  ?int   $offset
178     * @return array
179     */
180    public function getStates(string $sort = 'DESC', ?int $limit = null, ?int $offset = null): array
181    {
182        $files    = scandir($this->folder);
183        $fileList = [];
184        $results  = [];
185
186        foreach ($files as $file) {
187            if (($file != '.') && ($file != '..')) {
188                $fileList[] = ['name' => $file, 'timestamp' => $this->getFileTimestamp($file)];
189            }
190        }
191
192        usort($fileList, function ($a, $b) use ($sort) {
193            return ($sort == 'ASC') ? ($a['timestamp'] <=> $b['timestamp']) : ($b['timestamp'] <=> $a['timestamp']);
194        });
195
196        if ($limit !== null) {
197            $fileList = array_slice($fileList, (int)$offset, (int)$limit);
198        }
199
200        foreach ($fileList as $file) {
201            $results[$file['name']] = $this->decode($file['name']);
202        }
203
204        return $results;
205    }
206
207    /**
208     * Get model state by ID
209     *
210     * @param  int|string $id
211     * @return array
212     */
213    public function getStateById(int|string $id): array
214    {
215        $files   = scandir($this->folder);
216        $results = [];
217
218        foreach ($files as $file) {
219            if (str_contains($file, $id)) {
220                $results[$file] = $this->decode($file);
221                break;
222            }
223        }
224
225        return $results;
226    }
227
228    /**
229     * Get model state by model
230     *
231     * @param  string          $model
232     * @param  int|string|null $modelId
233     * @throws Exception
234     * @return array
235     */
236    public function getStateByModel(string $model, int|string|null $modelId = null): array
237    {
238        if ($modelId === null) {
239            throw new Exception('You must pass a model ID.');
240        }
241
242        $files   = scandir($this->folder);
243        $id      = md5($model . '-' . $modelId);
244        $results = [];
245
246        foreach ($files as $file) {
247            if (str_contains($file, $id)) {
248                $results[$file] = $this->decode($file);
249            }
250        }
251
252        return $results;
253    }
254
255    /**
256     * Get model state by timestamp
257     *
258     * @param  int  $from
259     * @param  ?int $backTo
260     * @return array
261     */
262    public function getStateByTimestamp(int $from, ?int $backTo = null): array
263    {
264        return $this->filterStatesByTimestamp($from, $backTo);
265    }
266
267    /**
268     * Get model state by date
269     *
270     * @param  string  $from
271     * @param  ?string $backTo
272     * @return array
273     */
274    public function getStateByDate(string $from, ?string $backTo = null): array
275    {
276        if (!str_contains($from, ' ')) {
277            $from .= ' 23:59:59';
278        }
279
280        $from = strtotime($from);
281
282        if ($backTo !== null) {
283            if (!str_contains($backTo, ' ')) {
284                $backTo .= ' 00:00:00';
285            }
286            $backTo = strtotime($backTo);
287        }
288
289        return $this->filterStatesByTimestamp($from, $backTo);
290    }
291
292    /**
293     * Get model snapshot by ID
294     *
295     * @param  int|string $id
296     * @param  bool       $post
297     * @return array
298     */
299     public function getSnapshot(int|string $id, bool $post = false): array
300     {
301         $result   = $this->getStateById($id);
302         $result   = reset($result);
303         $snapshot = [];
304
305         if (!($post) && !empty($result['old'])) {
306             $snapshot = $result['old'];
307         } else if (($post) && !empty($result['new'])) {
308             $snapshot = $result['new'];
309         }
310
311         return $snapshot;
312     }
313
314}