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\Log\Exception;
12use Laminas\Log\Filter;
13use Laminas\Log\FilterPluginManager as LogFilterPluginManager;
14use Laminas\Log\Formatter;
15use Laminas\Log\FormatterPluginManager as LogFormatterPluginManager;
16use Laminas\ServiceManager\ServiceManager;
17use Laminas\Stdlib\ErrorHandler;
18use Traversable;
19
20/**
21 * @todo Remove aliases for parent namespace's FilterPluginManager and
22 *    FormatterPluginManager once the deprecated versions in the current
23 *    namespace are removed (likely v3.0).
24 */
25abstract class AbstractWriter implements WriterInterface
26{
27    /**
28     * Filter plugins
29     *
30     * @var FilterPluginManager
31     */
32    protected $filterPlugins;
33
34    /**
35     * Formatter plugins
36     *
37     * @var FormatterPluginManager
38     */
39    protected $formatterPlugins;
40
41    /**
42     * Filter chain
43     *
44     * @var Filter\FilterInterface[]
45     */
46    protected $filters = [];
47
48    /**
49     * Formats the log message before writing
50     *
51     * @var Formatter\FormatterInterface
52     */
53    protected $formatter;
54
55    /**
56     * Use Laminas\Stdlib\ErrorHandler to report errors during calls to write
57     *
58     * @var bool
59     */
60    protected $convertWriteErrorsToExceptions = true;
61
62    /**
63     * Error level passed to Laminas\Stdlib\ErrorHandler::start for errors reported during calls to write
64     *
65     * @var bool
66     */
67    protected $errorsToExceptionsConversionLevel = E_WARNING;
68
69    /**
70     * Constructor
71     *
72     * Set options for a writer. Accepted options are:
73     * - filters: array of filters to add to this filter
74     * - formatter: formatter for this writer
75     *
76     * @param  array|Traversable $options
77     * @throws Exception\InvalidArgumentException
78     */
79    public function __construct($options = null)
80    {
81        if ($options instanceof Traversable) {
82            $options = iterator_to_array($options);
83        }
84
85        if (is_array($options)) {
86            if (isset($options['filter_manager'])) {
87                $this->setFilterPluginManager($options['filter_manager']);
88            }
89
90            if (isset($options['formatter_manager'])) {
91                $this->setFormatterPluginManager($options['formatter_manager']);
92            }
93
94            if (isset($options['filters'])) {
95                $filters = $options['filters'];
96                if (is_int($filters) || is_string($filters) || $filters instanceof Filter\FilterInterface) {
97                    $this->addFilter($filters);
98                } elseif (is_array($filters)) {
99                    foreach ($filters as $filter) {
100                        if (is_int($filter) || is_string($filter) || $filter instanceof Filter\FilterInterface) {
101                            $this->addFilter($filter);
102                        } elseif (is_array($filter)) {
103                            if (! isset($filter['name'])) {
104                                throw new Exception\InvalidArgumentException(
105                                    'Options must contain a name for the filter'
106                                );
107                            }
108                            $filterOptions = (isset($filter['options'])) ? $filter['options'] : null;
109                            $this->addFilter($filter['name'], $filterOptions);
110                        }
111                    }
112                }
113            }
114
115            if (isset($options['formatter'])) {
116                $formatter = $options['formatter'];
117                if (is_string($formatter) || $formatter instanceof Formatter\FormatterInterface) {
118                    $this->setFormatter($formatter);
119                } elseif (is_array($formatter)) {
120                    if (! isset($formatter['name'])) {
121                        throw new Exception\InvalidArgumentException('Options must contain a name for the formatter');
122                    }
123                    $formatterOptions = (isset($formatter['options'])) ? $formatter['options'] : null;
124                    $this->setFormatter($formatter['name'], $formatterOptions);
125                }
126            }
127        }
128    }
129
130    /**
131     * Add a filter specific to this writer.
132     *
133     * @param  int|string|Filter\FilterInterface $filter
134     * @param  array|null $options
135     * @return AbstractWriter
136     * @throws Exception\InvalidArgumentException
137     */
138    public function addFilter($filter, array $options = null)
139    {
140        if (is_int($filter)) {
141            $filter = new Filter\Priority($filter);
142        }
143
144        if (is_string($filter)) {
145            $filter = $this->filterPlugin($filter, $options);
146        }
147
148        if (! $filter instanceof Filter\FilterInterface) {
149            throw new Exception\InvalidArgumentException(sprintf(
150                'Filter must implement %s\Filter\FilterInterface; received "%s"',
151                __NAMESPACE__,
152                is_object($filter) ? get_class($filter) : gettype($filter)
153            ));
154        }
155
156        $this->filters[] = $filter;
157        return $this;
158    }
159
160    /**
161     * Get filter plugin manager
162     *
163     * @return LogFilterPluginManager
164     */
165    public function getFilterPluginManager()
166    {
167        if (null === $this->filterPlugins) {
168            $this->setFilterPluginManager(new LogFilterPluginManager(new ServiceManager()));
169        }
170        return $this->filterPlugins;
171    }
172
173    /**
174     * Set filter plugin manager
175     *
176     * @param  string|LogFilterPluginManager $plugins
177     * @return self
178     * @throws Exception\InvalidArgumentException
179     */
180    public function setFilterPluginManager($plugins)
181    {
182        if (is_string($plugins)) {
183            $plugins = new $plugins;
184        }
185        if (! $plugins instanceof LogFilterPluginManager) {
186            throw new Exception\InvalidArgumentException(sprintf(
187                'Writer plugin manager must extend %s; received %s',
188                LogFilterPluginManager::class,
189                is_object($plugins) ? get_class($plugins) : gettype($plugins)
190            ));
191        }
192
193        $this->filterPlugins = $plugins;
194        return $this;
195    }
196
197    /**
198     * Get filter instance
199     *
200     * @param string $name
201     * @param array|null $options
202     * @return Filter\FilterInterface
203     */
204    public function filterPlugin($name, array $options = null)
205    {
206        return $this->getFilterPluginManager()->get($name, $options);
207    }
208
209    /**
210     * Get formatter plugin manager
211     *
212     * @return LogFormatterPluginManager
213     */
214    public function getFormatterPluginManager()
215    {
216        if (null === $this->formatterPlugins) {
217            $this->setFormatterPluginManager(new LogFormatterPluginManager(new ServiceManager()));
218        }
219        return $this->formatterPlugins;
220    }
221
222    /**
223     * Set formatter plugin manager
224     *
225     * @param  string|LogFormatterPluginManager $plugins
226     * @return self
227     * @throws Exception\InvalidArgumentException
228     */
229    public function setFormatterPluginManager($plugins)
230    {
231        if (is_string($plugins)) {
232            $plugins = new $plugins;
233        }
234        if (! $plugins instanceof LogFormatterPluginManager) {
235            throw new Exception\InvalidArgumentException(
236                sprintf(
237                    'Writer plugin manager must extend %s; received %s',
238                    LogFormatterPluginManager::class,
239                    is_object($plugins) ? get_class($plugins) : gettype($plugins)
240                )
241            );
242        }
243
244        $this->formatterPlugins = $plugins;
245        return $this;
246    }
247
248    /**
249     * Get formatter instance
250     *
251     * @param string $name
252     * @param array|null $options
253     * @return Formatter\FormatterInterface
254     */
255    public function formatterPlugin($name, array $options = null)
256    {
257        return $this->getFormatterPluginManager()->get($name, $options);
258    }
259
260    /**
261     * Log a message to this writer.
262     *
263     * @param array $event log data event
264     * @return void
265     */
266    public function write(array $event)
267    {
268        foreach ($this->filters as $filter) {
269            if (! $filter->filter($event)) {
270                return;
271            }
272        }
273
274        $errorHandlerStarted = false;
275
276        if ($this->convertWriteErrorsToExceptions && ! ErrorHandler::started()) {
277            ErrorHandler::start($this->errorsToExceptionsConversionLevel);
278            $errorHandlerStarted = true;
279        }
280
281        try {
282            $this->doWrite($event);
283        } catch (\Exception $e) {
284            if ($errorHandlerStarted) {
285                ErrorHandler::stop();
286            }
287            throw $e;
288        }
289
290        if ($errorHandlerStarted) {
291            $error = ErrorHandler::stop();
292            if ($error) {
293                throw new Exception\RuntimeException("Unable to write", 0, $error);
294            }
295        }
296    }
297
298    /**
299     * Set a new formatter for this writer
300     *
301     * @param  string|Formatter\FormatterInterface $formatter
302     * @param  array|null $options
303     * @return self
304     * @throws Exception\InvalidArgumentException
305     */
306    public function setFormatter($formatter, array $options = null)
307    {
308        if (is_string($formatter)) {
309            $formatter = $this->formatterPlugin($formatter, $options);
310        }
311
312        if (! $formatter instanceof Formatter\FormatterInterface) {
313            throw new Exception\InvalidArgumentException(sprintf(
314                'Formatter must implement %s\Formatter\FormatterInterface; received "%s"',
315                __NAMESPACE__,
316                is_object($formatter) ? get_class($formatter) : gettype($formatter)
317            ));
318        }
319
320        $this->formatter = $formatter;
321        return $this;
322    }
323
324    /**
325     * Get formatter
326     *
327     * @return Formatter\FormatterInterface
328     */
329    protected function getFormatter()
330    {
331        return $this->formatter;
332    }
333
334    /**
335     * Check if the writer has a formatter
336     *
337     * @return bool
338     */
339    protected function hasFormatter()
340    {
341        return $this->formatter instanceof Formatter\FormatterInterface;
342    }
343
344    /**
345     * Set convert write errors to exception flag
346     *
347     * @param bool $convertErrors
348     */
349    public function setConvertWriteErrorsToExceptions($convertErrors)
350    {
351        $this->convertWriteErrorsToExceptions = $convertErrors;
352    }
353
354    /**
355     * Perform shutdown activities such as closing open resources
356     *
357     * @return void
358     */
359    public function shutdown()
360    {
361    }
362
363    /**
364     * Write a message to the log
365     *
366     * @param array $event log data event
367     * @return void
368     */
369    abstract protected function doWrite(array $event);
370}
371