1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Consistency;
38
39use Hoa\Event;
40use Hoa\Stream;
41
42/**
43 * Class Hoa\Consistency\Xcallable.
44 *
45 * Build a callable object, i.e. function, class::method, object->method or
46 * closure, they all have the same behaviour. This callable is an extension of
47 * native PHP callable (aka callback) to integrate Hoa's structures.
48 *
49 * @copyright  Copyright © 2007-2017 Hoa community
50 * @license    New BSD License
51 */
52class Xcallable
53{
54    /**
55     * Callback, with the PHP format.
56     *
57     * @var mixed
58     */
59    protected $_callback = null;
60
61    /**
62     * Callable hash.
63     *
64     * @var string
65     */
66    protected $_hash     = null;
67
68
69
70    /**
71     * Build a callback.
72     * Accepted forms:
73     *     * `'function'`,
74     *     * `'class::method'`,
75     *     * `'class', 'method'`,
76     *     * `$object, 'method'`,
77     *     * `$object, ''`,
78     *     * `function (…) { … }`,
79     *     * `['class', 'method']`,
80     *     * `[$object, 'method']`.
81     *
82     * @param   mixed   $call    First callable part.
83     * @param   mixed   $able    Second callable part (if needed).
84     */
85    public function __construct($call, $able = '')
86    {
87        if ($call instanceof \Closure) {
88            $this->_callback = $call;
89
90            return;
91        }
92
93        if (!is_string($able)) {
94            throw new Exception(
95                'Bad callback form; the able part must be a string.',
96                0
97            );
98        }
99
100        if ('' === $able) {
101            if (is_string($call)) {
102                if (false === strpos($call, '::')) {
103                    if (!function_exists($call)) {
104                        throw new Exception(
105                            'Bad callback form; function %s does not exist.',
106                            1,
107                            $call
108                        );
109                    }
110
111                    $this->_callback = $call;
112
113                    return;
114                }
115
116                list($call, $able) = explode('::', $call);
117            } elseif (is_object($call)) {
118                if ($call instanceof Stream\IStream\Out) {
119                    $able = null;
120                } elseif (method_exists($call, '__invoke')) {
121                    $able = '__invoke';
122                } else {
123                    throw new Exception(
124                        'Bad callback form; an object but without a known ' .
125                        'method.',
126                        2
127                    );
128                }
129            } elseif (is_array($call) && isset($call[0])) {
130                if (!isset($call[1])) {
131                    return $this->__construct($call[0]);
132                }
133
134                return $this->__construct($call[0], $call[1]);
135            } else {
136                throw new Exception(
137                    'Bad callback form.',
138                    3
139                );
140            }
141        }
142
143        $this->_callback = [$call, $able];
144
145        return;
146    }
147
148    /**
149     * Call the callable.
150     *
151     * @param   ...
152     * @return  mixed
153     */
154    public function __invoke()
155    {
156        $arguments = func_get_args();
157        $valid     = $this->getValidCallback($arguments);
158
159        return call_user_func_array($valid, $arguments);
160    }
161
162    /**
163     * Distribute arguments according to an array.
164     *
165     * @param   array  $arguments    Arguments.
166     * @return  mixed
167     */
168    public function distributeArguments(array $arguments)
169    {
170        return call_user_func_array([$this, '__invoke'], $arguments);
171    }
172
173    /**
174     * Get a valid callback in the PHP meaning.
175     *
176     * @param   array   &$arguments    Arguments (could determine method on an
177     *                                 object if not precised).
178     * @return  mixed
179     */
180    public function getValidCallback(array &$arguments = [])
181    {
182        $callback = $this->_callback;
183        $head     = null;
184
185        if (isset($arguments[0])) {
186            $head = &$arguments[0];
187        }
188
189        // If method is undetermined, we find it (we understand event bucket and
190        // stream).
191        if (null !== $head &&
192            is_array($callback) &&
193            null === $callback[1]) {
194            if ($head instanceof Event\Bucket) {
195                $head = $head->getData();
196            }
197
198            switch ($type = gettype($head)) {
199                case 'string':
200                    if (1 === strlen($head)) {
201                        $method = 'writeCharacter';
202                    } else {
203                        $method = 'writeString';
204                    }
205
206                    break;
207
208                case 'boolean':
209                case 'integer':
210                case 'array':
211                    $method = 'write' . ucfirst($type);
212
213                    break;
214
215                case 'double':
216                    $method = 'writeFloat';
217
218                    break;
219
220                default:
221                    $method = 'writeAll';
222                    $head   = $head . "\n";
223            }
224
225            $callback[1] = $method;
226        }
227
228        return $callback;
229    }
230
231    /**
232     * Get hash.
233     * Will produce:
234     *     * function#…;
235     *     * class#…::…;
236     *     * object(…)#…::…;
237     *     * closure(…).
238     *
239     * @return  string
240     */
241    public function getHash()
242    {
243        if (null !== $this->_hash) {
244            return $this->_hash;
245        }
246
247        $_ = &$this->_callback;
248
249        if (is_string($_)) {
250            return $this->_hash = 'function#' . $_;
251        }
252
253        if (is_array($_)) {
254            return
255                $this->_hash =
256                    (is_object($_[0])
257                        ? 'object(' . spl_object_hash($_[0]) . ')' .
258                          '#' . get_class($_[0])
259                        : 'class#' . $_[0]) .
260                    '::' .
261                    (null !== $_[1]
262                        ? $_[1]
263                        : '???');
264        }
265
266        return $this->_hash = 'closure(' . spl_object_hash($_) . ')';
267    }
268
269    /**
270     * Get appropriated reflection instance.
271     *
272     * @param   ...
273     * @return  \Reflector
274     */
275    public function getReflection()
276    {
277        $arguments = func_get_args();
278        $valid     = $this->getValidCallback($arguments);
279
280        if (is_string($valid)) {
281            return new \ReflectionFunction($valid);
282        }
283
284        if ($valid instanceof \Closure) {
285            return new \ReflectionFunction($valid);
286        }
287
288        if (is_array($valid)) {
289            if (is_string($valid[0])) {
290                if (false === method_exists($valid[0], $valid[1])) {
291                    return new \ReflectionClass($valid[0]);
292                }
293
294                return new \ReflectionMethod($valid[0], $valid[1]);
295            }
296
297            $object = new \ReflectionObject($valid[0]);
298
299            if (null === $valid[1]) {
300                return $object;
301            }
302
303            return $object->getMethod($valid[1]);
304        }
305    }
306
307    /**
308     * Return the hash.
309     *
310     * @return  string
311     */
312    public function __toString()
313    {
314        return $this->getHash();
315    }
316}
317