1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-log for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-log/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-log/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Log\Writer;
10
11use Laminas\Db\Adapter\Adapter;
12use Laminas\Log\Exception;
13use Laminas\Log\Formatter\Db as DbFormatter;
14use Traversable;
15
16class Db extends AbstractWriter
17{
18    /**
19     * Db adapter instance
20     *
21     * @var Adapter
22     */
23    protected $db;
24
25    /**
26     * Table name
27     *
28     * @var string
29     */
30    protected $tableName;
31
32    /**
33     * Relates database columns names to log data field keys.
34     *
35     * @var null|array
36     */
37    protected $columnMap;
38
39    /**
40     * Field separator for sub-elements
41     *
42     * @var string
43     */
44    protected $separator = '_';
45
46    /**
47     * Constructor
48     *
49     * We used the Adapter instead of Laminas\Db for a performance reason.
50     *
51     * @param Adapter|array|Traversable $db
52     * @param string $tableName
53     * @param array $columnMap
54     * @param string $separator
55     * @throws Exception\InvalidArgumentException
56     */
57    public function __construct($db, $tableName = null, array $columnMap = null, $separator = null)
58    {
59        if ($db instanceof Traversable) {
60            $db = iterator_to_array($db);
61        }
62
63        if (is_array($db)) {
64            parent::__construct($db);
65            $separator = isset($db['separator']) ? $db['separator'] : null;
66            $columnMap = isset($db['column']) ? $db['column'] : null;
67            $tableName = isset($db['table']) ? $db['table'] : null;
68            $db        = isset($db['db']) ? $db['db'] : null;
69        }
70
71        if (! $db instanceof Adapter) {
72            throw new Exception\InvalidArgumentException('You must pass a valid Laminas\Db\Adapter\Adapter');
73        }
74
75        $tableName = (string) $tableName;
76        if ('' === $tableName) {
77            throw new Exception\InvalidArgumentException(
78                'You must specify a table name. Either directly in the constructor, or via options'
79            );
80        }
81
82        $this->db        = $db;
83        $this->tableName = $tableName;
84        $this->columnMap = $columnMap;
85
86        if (! empty($separator)) {
87            $this->separator = $separator;
88        }
89
90        if (! $this->hasFormatter()) {
91            $this->setFormatter(new DbFormatter());
92        }
93    }
94
95    /**
96     * Remove reference to database adapter
97     *
98     * @return void
99     */
100    public function shutdown()
101    {
102        $this->db = null;
103    }
104
105    /**
106     * Write a message to the log.
107     *
108     * @param array $event event data
109     * @return void
110     * @throws Exception\RuntimeException
111     */
112    protected function doWrite(array $event)
113    {
114        if (null === $this->db) {
115            throw new Exception\RuntimeException('Database adapter is null');
116        }
117
118        $event = $this->formatter->format($event);
119
120        // Transform the event array into fields
121        if (null === $this->columnMap) {
122            $dataToInsert = $this->eventIntoColumn($event);
123        } else {
124            $dataToInsert = $this->mapEventIntoColumn($event, $this->columnMap);
125        }
126
127        $statement = $this->db->query($this->prepareInsert($dataToInsert));
128        $statement->execute($dataToInsert);
129    }
130
131    /**
132     * Prepare the INSERT SQL statement
133     *
134     * @param  array $fields
135     * @return string
136     */
137    protected function prepareInsert(array $fields)
138    {
139        $keys = array_keys($fields);
140        $sql = 'INSERT INTO ' . $this->db->platform->quoteIdentifier($this->tableName) . ' (' .
141            implode(",", array_map([$this->db->platform, 'quoteIdentifier'], $keys)) . ') VALUES (' .
142            implode(",", array_map([$this->db->driver, 'formatParameterName'], $keys)) . ')';
143
144        return $sql;
145    }
146
147    /**
148     * Map event into column using the $columnMap array
149     *
150     * @param  array $event
151     * @param  array $columnMap
152     * @return array
153     */
154    protected function mapEventIntoColumn(array $event, array $columnMap = null)
155    {
156        if (empty($event)) {
157            return [];
158        }
159
160        $data = [];
161        foreach ($event as $name => $value) {
162            if (is_array($value)) {
163                foreach ($value as $key => $subvalue) {
164                    if (isset($columnMap[$name][$key])) {
165                        if (is_scalar($subvalue)) {
166                            $data[$columnMap[$name][$key]] = $subvalue;
167                            continue;
168                        }
169
170                        $data[$columnMap[$name][$key]] = var_export($subvalue, true);
171                    }
172                }
173            } elseif (isset($columnMap[$name])) {
174                $data[$columnMap[$name]] = $value;
175            }
176        }
177        return $data;
178    }
179
180    /**
181     * Transform event into column for the db table
182     *
183     * @param  array $event
184     * @return array
185     */
186    protected function eventIntoColumn(array $event)
187    {
188        if (empty($event)) {
189            return [];
190        }
191
192        $data = [];
193        foreach ($event as $name => $value) {
194            if (is_array($value)) {
195                foreach ($value as $key => $subvalue) {
196                    if (is_scalar($subvalue)) {
197                        $data[$name . $this->separator . $key] = $subvalue;
198                        continue;
199                    }
200
201                    $data[$name . $this->separator . $key] = var_export($subvalue, true);
202                }
203            } else {
204                $data[$name] = $value;
205            }
206        }
207        return $data;
208    }
209}
210