Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Table
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
12 / 12
30
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
2
 setTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 send
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getStates
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getStateById
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getStateByModel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getStateByTimestamp
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getStateByDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 decodeState
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 getSnapshot
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 createTable
100.00% covered (success)
100.00%
20 / 20
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 <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
17use Pop\Db\Record;
18
19/**
20 * Auditor table class
21 *
22 * @category   Pop
23 * @package    Pop\Audit
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    3.0.0
28 */
29class Table extends AbstractAdapter
30{
31
32    /**
33     * Table class name
34     * @var ?string
35     */
36    protected ?string $table = null;
37
38    /**
39     * Constructor
40     *
41     * Instantiate the table adapter object
42     *
43     * @param  string $table
44     */
45    public function __construct(string $table)
46    {
47        $this->setTable($table);
48        $tableClass = $this->table;
49        $db         = $tableClass::getDb();
50        $tableName  = $tableClass::table();
51
52        if (!($db->hasTable($tableName))) {
53            $this->createTable($tableName);
54        }
55    }
56
57    /**
58     * Set the table
59     *
60     * @param  string $table
61     * @return Table
62     */
63    public function setTable(string $table): Table
64    {
65        $this->table = $table;
66        return $this;
67    }
68
69    /**
70     * Get the table
71     *
72     * @return string
73     */
74    public function getTable(): string
75    {
76        return $this->table;
77    }
78
79    /**
80     * Send the results of the audit
81     *
82     * @throws Exception
83     * @return Record
84     */
85    public function send(): Record
86    {
87        if ($this->action === null) {
88            throw new Exception('The model state differences have not been resolved.');
89        }
90        if (($this->model === null) || ($this->modelId === null)) {
91            throw new Exception('The model has not been set.');
92        }
93
94        $className = $this->table;
95        $table     = new $className($this->prepareData());
96        $table->save();
97
98        return $table;
99    }
100
101    /**
102     * Get model states
103     *
104     * @param  ?array $columns
105     * @param  ?array $options
106     * @return array
107     */
108    public function getStates(?array $columns = null, ?array $options = null): array
109    {
110        $tableClass = $this->table;
111        $result     = ($columns !== null) ? $tableClass::findBy($columns, $options) : $tableClass::findAll($options);
112
113        return array_map([$this, 'decodeState'], $result->toArray());
114    }
115
116    /**
117     * Get model state by ID
118     *
119     * @param  int|string $id
120     * @return array
121     */
122    public function getStateById(int|string $id): array
123    {
124        $tableClass = $this->table;
125        $record     = $tableClass::findById($id);
126        return $this->decodeState($record->toArray());
127    }
128
129    /**
130     * Get model state by model
131     *
132     * @param  string          $model
133     * @param  int|string|null $modelId
134     * @param  array           $columns
135     * @return array
136     */
137    public function getStateByModel(string $model, int|string|null $modelId = null, array $columns = []): array
138    {
139        $columns['model']    = $model;
140        if ($modelId !== null) {
141            $columns['model_id'] = $modelId;
142        }
143        $tableClass = $this->table;
144        $result     = $tableClass::findBy($columns);
145        return array_map([$this, 'decodeState'], $result->toArray());
146    }
147
148    /**
149     * Get model state by timestamp
150     *
151     * @param  int   $from
152     * @param  ?int  $backTo
153     * @param  array $columns
154     * @return array
155     */
156    public function getStateByTimestamp(int $from, ?int $backTo = null, array $columns = []): array
157    {
158        $to = date('Y-m-d H:i:s', $from);
159        $columns['timestamp'] = ($backTo !== null) ?
160            ['BETWEEN', date('Y-m-d H:i:s', $backTo), $to] : ['<=', $to];
161
162        $tableClass = $this->table;
163        $result     = $tableClass::findBy($columns);
164        return array_map([$this, 'decodeState'], $result->toArray());
165    }
166
167    /**
168     * Get model state by date
169     *
170     * @param  string  $from
171     * @param  ?string $backTo
172     * @param  array   $columns
173     * @return array
174     */
175    public function getStateByDate(string $from, ?string $backTo = null, array $columns = []): array
176    {
177        if (!str_contains($from, ' ')) {
178            $from .= ' 23:59:59';
179        }
180        if ($backTo !== null) {
181            if (!str_contains($backTo, ' ')) {
182                $backTo .= ' 00:00:00';
183            }
184            $columns['timestamp'] = ['BETWEEN', $backTo, $from];
185        } else {
186            $columns['timestamp'] = ['<=', $from];
187        }
188        $tableClass = $this->table;
189        $result     = $tableClass::findBy($columns);
190        return array_map([$this, 'decodeState'], $result->toArray());
191    }
192
193    /**
194     * Decode the JSON-encoded old/new state fields of a result row
195     *
196     * @param  array $result
197     * @return array
198     */
199    protected function decodeState(array $result): array
200    {
201        if (!empty($result['old']) && is_string($result['old'])) {
202            $result['old'] = json_decode($result['old'], true);
203        }
204        if (!empty($result['new']) && is_string($result['new'])) {
205            $result['new'] = json_decode($result['new'], true);
206        }
207
208        return $result;
209    }
210
211    /**
212     * Get model snapshot by ID
213     *
214     * @param  int|string $id
215     * @param  bool       $post
216     * @return array
217     */
218    public function getSnapshot(int|string $id, bool $post = false): array
219    {
220        $tableClass = $this->table;
221        $result     = $this->decodeState($tableClass::findById($id)->toArray());
222        $snapshot   = [];
223
224        if (!($post) && !empty($result['old'])) {
225            $snapshot = $result['old'];
226        } else if (($post) && !empty($result['new'])) {
227            $snapshot = $result['new'];
228        }
229
230        return $snapshot;
231    }
232
233    /**
234     * Create table in database
235     *
236     * @param  string $tableName
237     * @return void
238     */
239    protected function createTable(string $tableName): void
240    {
241        $tableClass = $this->table;
242        $db         = $tableClass::getDb();
243        $schema     = $db->createSchema();
244        $schema->create($tableName)
245            ->int('id')->increment()
246            ->int('user_id')
247            ->varchar('username', 255)
248            ->varchar('domain', 255)
249            ->varchar('route', 255)
250            ->varchar('method', 255)
251            ->varchar('model', 255)->notNullable()
252            ->int('model_id')->notNullable()
253            ->varchar('action', 255)->notNullable()
254            ->text('old')
255            ->text('new')
256            ->text('state')
257            ->text('metadata')
258            ->datetime('timestamp')->notNullable()
259            ->primary('id');
260
261        $db->query($schema);
262    }
263
264}