Passed
Push — master ( e5ddcf...d13167 )
by Arthur
02:06
created

Format::forAll()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 4
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace QueryPath\Extension;
4
5
use QueryPath\DOMQuery;
6
use QueryPath\Extension;
7
use QueryPath\Query;
8
use QueryPath\Exception;
9
10
/**
11
 * A QueryPath extension that adds extra methods for formatting node values.
12
 *
13
 * This extension provides two methods:
14
 *
15
 * - format()
16
 * - formatAttr()
17
 *
18
 * Usage:
19
 * <code>
20
 * <?php
21
 * QueryPath::enable('Noi\QueryPath\FormatExtension');
22
 * $qp = qp('<?xml version="1.0"?><root><item score="12000">TEST A</item><item score="9876.54">TEST B</item></root>');
23
 *
24
 * $qp->find('item')->format(function ($text) {
25
 *     return ucwords(strtolower($text));
26
 * });
27
 * $qp->find('item')->formatAttr('score', 'number_format', 2);
28
 *
29
 * $qp->writeXML();
30
 * </code>
31
 *
32
 * OUTPUT:
33
 * <code>
34
 * <?xml version="1.0"?>
35
 * <root>
36
 *   <item score="12,000.00">Test A</item>
37
 *   <item score="9,876.54">Test B</item>
38
 * </root>
39
 * </code>
40
 *
41
 * @see FormatExtension::format()
42
 * @see FormatExtension::formatAttr()
43
 *
44
 * @author Akihiro Yamanoi <[email protected]>
45
 */
46
class Format implements Extension
47
{
48
    protected $qp;
49
50
    public function __construct(Query $qp)
51
    {
52
        $this->qp = $qp;
53
    }
54
55
    /**
56
     * Formats the text content of each selected element in the current DOMQuery object.
57
     *
58
     * Usage:
59
     * <code>
60
     * <?php
61
     * QueryPath::enable('Noi\QueryPath\FormatExtension');
62
     * $qp = qp('<?xml version="1.0"?><root><div>Apple</div><div>Orange</div></root>');
63
     *
64
     * $qp->find('div')->format('strtoupper');
65
     * $qp->find('div')->format(function ($text) {
66
     *     return '*' . $text . '*';
67
     * });
68
     *
69
     * $qp->writeXML();
70
     * </code>
71
     *
72
     * OUTPUT:
73
     * <code>
74
     * <?xml version="1.0"?>
75
     * <root>
76
     *   <div>*APPLE*</div>
77
     *   <div>*ORANGE*</div>
78
     * </root>
79
     * </code>
80
     *
81
     * @param callable $callback The callable to be called on every element.
82
     * @param mixed $args        [optional] Zero or more parameters to be passed to the callback.
83
     * @param null $additional
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $additional is correct as it would always require null to be passed?
Loading history...
84
     * @return DOMQuery The DOMQuery object with the same element(s) selected.
85
     * @throws Exception
86
     */
87
    public function format($callback, $args = null, $additional = null): Query
88
    {
89
        if (isset($additional)) {
90
            $args = func_get_args();
91
            array_shift($args);
92
        }
93
94
        $getter = function ($qp) {
95
            return $qp->text();
96
        };
97
98
        $setter = function ($qp, $value) {
99
            $qp->text($value);
100
        };
101
102
        return $this->forAll($callback, $args, $getter, $setter);
103
    }
104
105
    /**
106
     * Formats the given attribute of each selected element in the current DOMQuery object.
107
     *
108
     * Usage:
109
     * <code>
110
     * QueryPath::enable('Noi\QueryPath\FormatExtension');
111
     * $qp = qp('<?xml version="1.0"?><root><item label="_apple_" total="12,345,678" /><item label="_orange_" total="987,654,321" /></root>');
112
     *
113
     * $qp->find('item')
114
     *     ->formatAttr('label', 'trim', '_')
115
     *     ->formatAttr('total', 'str_replace[2]', ',', '');
116
     *
117
     * $qp->find('item')->formatAttr('label', function ($value) {
118
     *     return ucfirst(strtolower($value));
119
     * });
120
     *
121
     * $qp->writeXML();
122
     * </code>
123
     *
124
     * OUTPUT:
125
     * <code>
126
     * <?xml version="1.0"?>
127
     * <root>
128
     *   <item label="Apple" total="12345678"/>
129
     *   <item label="Orange" total="987654321"/>
130
     * </root>
131
     * </code>
132
     *
133
     * @param string $attrName   The attribute name.
134
     * @param callable $callback The callable to be called on every element.
135
     * @param mixed $args        [optional] Zero or more parameters to be passed to the callback.
136
     * @param null $additional
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $additional is correct as it would always require null to be passed?
Loading history...
137
     * @return DOMQuery The DOMQuery object with the same element(s) selected.
138
     * @throws Exception
139
     */
140
    public function formatAttr($attrName, $callback, $args = null, $additional = null): Query
141
    {
142
        if (isset($additional)) {
143
            $args = array_slice(func_get_args(), 2);
144
        }
145
146
        $getter = function ($qp) use ($attrName) {
147
            return $qp->attr($attrName);
148
        };
149
150
        $setter = function ($qp, $value) use ($attrName) {
151
            return $qp->attr($attrName, $value);
152
        };
153
154
        return $this->forAll($callback, $args, $getter, $setter);
155
    }
156
157
    /**
158
     * @param $callback
159
     * @param $args
160
     * @param $getter
161
     * @param $setter
162
     * @return Query
163
     * @throws Exception
164
     */
165
    protected function forAll($callback, $args, $getter, $setter): Query
166
    {
167
        [$callback, $pos] = $this->prepareCallback($callback);
168
        if (!is_callable($callback)) {
169
            throw new Exception('Callback is not callable.');
170
        }
171
172
        $padded = $this->prepareArgs($args, $pos);
173
        foreach ($this->qp as $qp) {
174
            $padded[$pos] = $getter($qp);
175
            $setter($qp, call_user_func_array($callback, $padded));
176
        }
177
178
        return $this->qp;
179
    }
180
181
    /**
182
     * @param $callback
183
     * @return array
184
     */
185
    protected function prepareCallback($callback)
186
    {
187
        if (is_string($callback)) {
188
            [$callback, $trail] = $this->splitFunctionName($callback);
189
            $pos = (int)$trail;
190
        } elseif (is_array($callback) && isset($callback[2])) {
191
            $pos = $callback[2];
192
            $callback = array($callback[0], $callback[1]);
193
        } else {
194
            $pos = 0;
195
        }
196
        return array($callback, $pos);
197
    }
198
199
    /**
200
     * @param string $string
201
     * @return array[]|false|string[]
202
     */
203
    protected function splitFunctionName(string $string)
204
    {
205
        // 'func_name:2', 'func_name@3', 'func_name[1]', ...
206
        return preg_split('/[^a-zA-Z0-9_\x7f-\xff][^\d]*|$/', $string, 2);
207
    }
208
209
    /**
210
     * @param $args
211
     * @param $pos
212
     * @return array
213
     */
214
    protected function prepareArgs($args, $pos): array
215
    {
216
        $padded = array_pad((array) $args, (0 < $pos) ? $pos - 1 : 0, null);
217
        array_splice($padded, $pos, 0, array(null)); // insert null as a place holder
218
        return $padded;
219
    }
220
}