Completed
Push — master ( 7be8a5...d24bb0 )
by Ben
05:26
created

Formatter::format()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 2 Features 0
Metric Value
c 10
b 2
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 7
nc 4
nop 2
1
<?php
2
3
namespace Benrowe\Formatter;
4
5
use \ReflectionObject;
6
use \ReflectionMethod;
7
use \Closure;
8
use InvalidArgumentException;
9
10
/**
11
 * Formatter
12
 * Enables values to be formatted
13
 *
14
 * @package Benrowe\Formatter
15
 */
16
class Formatter extends AbstractFormatterProvider
17
{
18
    /**
19
     * If no formatter is specified, this formatter is used by default
20
     *
21
     * @var string
22
     */
23
    protected $defaultFormatter;
24
25
    /**
26
     * The list of available formatter providers.
27
     * The key is the same key that is exposed in the format() method
28
     * The value is either a Closure, a FQC, or an object that implements the
29
     * FormatterProvider interface
30
     *
31
     * @var array list of formatters & providers|closures
32
     */
33
    private $providers = [];
34
35
    /**
36
     * A list of all the available formats. If a formatter is an instance of
37
     * FormatterProvider, it's list is exploded using dot notiation.
38
     *
39
     * @var string[]
40
     */
41
    private $formats = [];
42
43
    private $formatMethodPrefix = 'as';
44
45
    /**
46
     * Constructor
47
     *
48
     * @param array $formatters The formatters to provide, either as instances
49
     *                          of FormatterProvider or closures
50
     */
51
    public function __construct(array $formatters = [])
52
    {
53
        foreach ($formatters as $formatter => $closure) {
54
            $this->addFormatter($formatter, $closure);
55
        }
56
    }
57
58
    /**
59
     * Set the default formatter to use
60
     *
61
     * @param string $format
62
     */
63
    public function setDefaultFormatter($format)
64
    {
65
        if (!$this->hasFormat($format)) {
66
            throw new InvalidArgumentException(
67
                'format "'.$format.'" does not exist'
68
            );
69
        }
70
        $this->defaultFormatter = $format;
71
    }
72
73
    /**
74
     * Get the default formatter
75
     *
76
     * @return string
77
     */
78
    public function getDefaultFormatter()
79
    {
80
        return $this->defaultFormatter;
81
    }
82
83
    /**
84
     * Add a new or replace a formatter within the stack
85
     *
86
     * @param string $name   The name of the formatter
87
     * @param Closure|FormatterProvider $method the object executes the format
88
     * @throws InvalidArgumentException
89
     */
90
    public function addFormatter($name, $method)
91
    {
92
        $this->validateProviderName($name);
93
        $method = $this->getFormatterObject($method);
94
95
        if (!($method instanceof FormatterProvider || $method instanceof Closure)) {
96
            throw new InvalidArgumentException('Supplied formatter is not supported');
97
        }
98
        $name = strtolower($name);
99
        $this->providers[$name] = $method;
100
101
        // generate a list of formats from this method
102
        $this->formats = array_merge(
103
            $this->formats,
104
            $this->getFormatsFromFormatter($method, $name)
105
        );
106
107
        $this->checkDefaultFormatter();
108
    }
109
110
    /**
111
     * Detect and convert the FQN of a formatter provider into an instance
112
     *
113
     * @param  FormatterProvider|Closure|string $formatter
114
     * @return FormatterProvider|Closure
0 ignored issues
show
Documentation introduced by
Should the return type not be string|object?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
115
     */
116
    private function getFormatterObject($formatter)
117
    {
118
        if (is_string($formatter) && class_exists($formatter)) {
119
            $formatter = new $formatter;
120
        }
121
        return $formatter;
122
    }
123
124
    /**
125
     * Check the default format and set the default if we have at least
126
     * one formatter
127
     *
128
     * @return nil
0 ignored issues
show
Documentation introduced by
Should the return type not be nil|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
129
     */
130
    private function checkDefaultFormatter()
131
    {
132
        if (!$this->defaultFormatter) {
133
            $format = current($this->formats);
134
            if ($format) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $format of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
135
                $this->setDefaultFormatter($format);
136
            }
137
        }
138
    }
139
140
    /**
141
     * Get a list of available formats from the supplied formatter
142
     *
143
     * @param  Closure|FormatterProvider $formatter
144
     * @param  string $name Base name of the formatter
145
     * @return string[]
146
     */
147
    private function getFormatsFromFormatter($formatter, $name)
148
    {
149
        if ($formatter instanceof Closure) {
150
            return [$name];
151
        }
152
        $formats = $formatter->formats();
153
        // prefix each formatter of the object with the name of the formatter
154
        return array_map(function ($value) use ($name) {
155
            return $name . '.' . $value;
156
        }, $formats);
157
    }
158
159
    /**
160
     * Format the provided value based on the requested formatter
161
     *
162
     * @param mixed $value The value to format
163
     * @param string|array The format + format options, if an array is provided the first value is the formatter
164
     *                     and the other values are format params
165
     * @return mixed
166
     * @throws InvalidArgumentException
167
     */
168
    public function format($value, $format = null)
169
    {
170
        $format = $format ?: $this->defaultFormatter;
171
172
        list($format, $params) = $this->extractFormatAndParams($value, $format);
173
174
        if (!$this->hasFormat($format)) {
175
            throw new InvalidArgumentException(
176
                'Unknown format: "' . $format . '"'
177
            );
178
        }
179
180
        return $this->callFormatter($format, $params);
181
    }
182
183
    /**
184
     * Get the current list of available formats
185
     *
186
     * @return array
187
     */
188
    public function formats()
189
    {
190
        return $this->formats;
191
    }
192
193
    /**
194
     * Allow dynamic calls to be made to the formatter
195
     *
196
     * @todo Is this still needed?
197
     */
198
    public function __call($method, $params)
199
    {
200
        $format = strtolower(substr($method, strlen($this->formatMethodPrefix)));
201
        $value = array_shift($params);
202
        array_unshift($params, $format);
203
        return $this->format($value, $params);
204
    }
205
206
    /**
207
     * Determine if the format exists within the formatter.
208
     *
209
     * @return boolean
210
     * @throws InvalidArgumentException
211
     */
212
    public function hasFormat($format)
213
    {
214
        if (!preg_match("/^[A-Za-z]+(\.[A-Za-z]+)?$/", $format)) {
215
            throw new InvalidArgumentException(
216
                'Format "' . $format . '" is not provided in correct format'
217
            );
218
        }
219
        return in_array(strtolower($format), $this->formats);
220
    }
221
222
    /**
223
     * Validate the provider name
224
     *
225
     * @param  string $name
226
     * @return boolean
227
     */
228
    private function validateProviderName($name)
229
    {
230
        if (!preg_match("/^[\w]+$/", $name)) {
231
            throw new InvalidArgumentException(
232
                'Supplied formatter name "'.$name.'" contains invalid characters'
233
            );
234
        }
235
        return true;
236
    }
237
238
    /**
239
     * Calls the requested formatting method based on it's simple formatting name
240
     * + passes through the requested params
241
     *
242
     * @param  string $format
243
     * @param  array  $params
244
     * @return mixed
245
     */
246
    private function callFormatter($format, array $params)
247
    {
248
        // is the formatter in a custom defined
249
        if (strpos($format, '.') > 0) {
250
            list($provider, $format) = explode($format, '.');
251
            $callback = $this->providers[$provider];
252
            $func = [$callback, 'format'];
253
            array_unshift($params, $format);
254
            $params = [$value, $params];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
255
        } else {
256
            // Closure
257
            $callback = $this->providers[$format];
258
            $func = $callback->bindTo($this);
259
        }
260
261
        return call_user_func_array($func, $params);
262
    }
263
}
264