1<?php
2/**
3 *  base include file for SimpleTest
4 *  @package    SimpleTest
5 *  @subpackage UnitTester
6 *  @version    $Id$
7 */
8/**
9 * does type matter
10 */
11if (! defined('TYPE_MATTERS')) {
12    define('TYPE_MATTERS', true);
13}
14
15/**
16 *    Displays variables as text and does diffs.
17 *    @package  SimpleTest
18 *    @subpackage   UnitTester
19 */
20class SimpleDumper {
21
22    /**
23     *    Renders a variable in a shorter form than print_r().
24     *    @param mixed $value      Variable to render as a string.
25     *    @return string           Human readable string form.
26     *    @access public
27     */
28    function describeValue($value) {
29        $type = $this->getType($value);
30        switch($type) {
31            case "Null":
32                return "NULL";
33            case "Boolean":
34                return "Boolean: " . ($value ? "true" : "false");
35            case "Array":
36                return "Array: " . count($value) . " items";
37            case "Object":
38                return "Object: of " . get_class($value);
39            case "String":
40                return "String: " . $this->clipString($value, 200);
41            default:
42                return "$type: $value";
43        }
44        return "Unknown";
45    }
46
47    /**
48     *    Gets the string representation of a type.
49     *    @param mixed $value    Variable to check against.
50     *    @return string         Type.
51     *    @access public
52     */
53    function getType($value) {
54        if (! isset($value)) {
55            return "Null";
56        } elseif (is_bool($value)) {
57            return "Boolean";
58        } elseif (is_string($value)) {
59            return "String";
60        } elseif (is_integer($value)) {
61            return "Integer";
62        } elseif (is_float($value)) {
63            return "Float";
64        } elseif (is_array($value)) {
65            return "Array";
66        } elseif (is_resource($value)) {
67            return "Resource";
68        } elseif (is_object($value)) {
69            return "Object";
70        }
71        return "Unknown";
72    }
73
74    /**
75     *    Creates a human readable description of the
76     *    difference between two variables. Uses a
77     *    dynamic call.
78     *    @param mixed $first        First variable.
79     *    @param mixed $second       Value to compare with.
80     *    @param boolean $identical  If true then type anomolies count.
81     *    @return string             Description of difference.
82     *    @access public
83     */
84    function describeDifference($first, $second, $identical = false) {
85        if ($identical) {
86            if (! $this->isTypeMatch($first, $second)) {
87                return "with type mismatch as [" . $this->describeValue($first) .
88                    "] does not match [" . $this->describeValue($second) . "]";
89            }
90        }
91        $type = $this->getType($first);
92        if ($type == "Unknown") {
93            return "with unknown type";
94        }
95        $method = 'describe' . $type . 'Difference';
96        return $this->$method($first, $second, $identical);
97    }
98
99    /**
100     *    Tests to see if types match.
101     *    @param mixed $first        First variable.
102     *    @param mixed $second       Value to compare with.
103     *    @return boolean            True if matches.
104     *    @access private
105     */
106    protected function isTypeMatch($first, $second) {
107        return ($this->getType($first) == $this->getType($second));
108    }
109
110    /**
111     *    Clips a string to a maximum length.
112     *    @param string $value         String to truncate.
113     *    @param integer $size         Minimum string size to show.
114     *    @param integer $position     Centre of string section.
115     *    @return string               Shortened version.
116     *    @access public
117     */
118    function clipString($value, $size, $position = 0) {
119        $length = strlen($value);
120        if ($length <= $size) {
121            return $value;
122        }
123        $position = min($position, $length);
124        $start = ($size/2 > $position ? 0 : $position - $size/2);
125        if ($start + $size > $length) {
126            $start = $length - $size;
127        }
128        $value = substr($value, $start, $size);
129        return ($start > 0 ? "..." : "") . $value . ($start + $size < $length ? "..." : "");
130    }
131
132    /**
133     *    Creates a human readable description of the
134     *    difference between two variables. The minimal
135     *    version.
136     *    @param null $first          First value.
137     *    @param mixed $second        Value to compare with.
138     *    @return string              Human readable description.
139     *    @access private
140     */
141    protected function describeGenericDifference($first, $second) {
142        return "as [" . $this->describeValue($first) .
143                "] does not match [" .
144                $this->describeValue($second) . "]";
145    }
146
147    /**
148     *    Creates a human readable description of the
149     *    difference between a null and another variable.
150     *    @param null $first          First null.
151     *    @param mixed $second        Null to compare with.
152     *    @param boolean $identical   If true then type anomolies count.
153     *    @return string              Human readable description.
154     *    @access private
155     */
156    protected function describeNullDifference($first, $second, $identical) {
157        return $this->describeGenericDifference($first, $second);
158    }
159
160    /**
161     *    Creates a human readable description of the
162     *    difference between a boolean and another variable.
163     *    @param boolean $first       First boolean.
164     *    @param mixed $second        Boolean to compare with.
165     *    @param boolean $identical   If true then type anomolies count.
166     *    @return string              Human readable description.
167     *    @access private
168     */
169    protected function describeBooleanDifference($first, $second, $identical) {
170        return $this->describeGenericDifference($first, $second);
171    }
172
173    /**
174     *    Creates a human readable description of the
175     *    difference between a string and another variable.
176     *    @param string $first        First string.
177     *    @param mixed $second        String to compare with.
178     *    @param boolean $identical   If true then type anomolies count.
179     *    @return string              Human readable description.
180     *    @access private
181     */
182    protected function describeStringDifference($first, $second, $identical) {
183        if (is_object($second) || is_array($second)) {
184            return $this->describeGenericDifference($first, $second);
185        }
186        $position = $this->stringDiffersAt($first, $second);
187        $message = "at character $position";
188        $message .= " with [" .
189                $this->clipString($first, 200, $position) . "] and [" .
190                $this->clipString($second, 200, $position) . "]";
191        return $message;
192    }
193
194    /**
195     *    Creates a human readable description of the
196     *    difference between an integer and another variable.
197     *    @param integer $first       First number.
198     *    @param mixed $second        Number to compare with.
199     *    @param boolean $identical   If true then type anomolies count.
200     *    @return string              Human readable description.
201     *    @access private
202     */
203    protected function describeIntegerDifference($first, $second, $identical) {
204        if (is_object($second) || is_array($second)) {
205            return $this->describeGenericDifference($first, $second);
206        }
207        return "because [" . $this->describeValue($first) .
208                "] differs from [" .
209                $this->describeValue($second) . "] by " .
210                abs($first - $second);
211    }
212
213    /**
214     *    Creates a human readable description of the
215     *    difference between two floating point numbers.
216     *    @param float $first         First float.
217     *    @param mixed $second        Float to compare with.
218     *    @param boolean $identical   If true then type anomolies count.
219     *    @return string              Human readable description.
220     *    @access private
221     */
222    protected function describeFloatDifference($first, $second, $identical) {
223        if (is_object($second) || is_array($second)) {
224            return $this->describeGenericDifference($first, $second);
225        }
226        return "because [" . $this->describeValue($first) .
227                "] differs from [" .
228                $this->describeValue($second) . "] by " .
229                abs($first - $second);
230    }
231
232    /**
233     *    Creates a human readable description of the
234     *    difference between two arrays.
235     *    @param array $first         First array.
236     *    @param mixed $second        Array to compare with.
237     *    @param boolean $identical   If true then type anomolies count.
238     *    @return string              Human readable description.
239     *    @access private
240     */
241    protected function describeArrayDifference($first, $second, $identical) {
242        if (! is_array($second)) {
243            return $this->describeGenericDifference($first, $second);
244        }
245        if (! $this->isMatchingKeys($first, $second, $identical)) {
246            return "as key list [" .
247                    implode(", ", array_keys($first)) . "] does not match key list [" .
248                    implode(", ", array_keys($second)) . "]";
249        }
250        foreach (array_keys($first) as $key) {
251            if ($identical && ($first[$key] === $second[$key])) {
252                continue;
253            }
254            if (! $identical && ($first[$key] == $second[$key])) {
255                continue;
256            }
257            return "with member [$key] " . $this->describeDifference(
258                    $first[$key],
259                    $second[$key],
260                    $identical);
261        }
262        return "";
263    }
264
265    /**
266     *    Compares two arrays to see if their key lists match.
267     *    For an identical match, the ordering and types of the keys
268     *    is significant.
269     *    @param array $first         First array.
270     *    @param array $second        Array to compare with.
271     *    @param boolean $identical   If true then type anomolies count.
272     *    @return boolean             True if matching.
273     *    @access private
274     */
275    protected function isMatchingKeys($first, $second, $identical) {
276        $first_keys = array_keys($first);
277        $second_keys = array_keys($second);
278        if ($identical) {
279            return ($first_keys === $second_keys);
280        }
281        sort($first_keys);
282        sort($second_keys);
283        return ($first_keys == $second_keys);
284    }
285
286    /**
287     *    Creates a human readable description of the
288     *    difference between a resource and another variable.
289     *    @param resource $first       First resource.
290     *    @param mixed $second         Resource to compare with.
291     *    @param boolean $identical    If true then type anomolies count.
292     *    @return string              Human readable description.
293     *    @access private
294     */
295    protected function describeResourceDifference($first, $second, $identical) {
296        return $this->describeGenericDifference($first, $second);
297    }
298
299    /**
300     *    Creates a human readable description of the
301     *    difference between two objects.
302     *    @param object $first        First object.
303     *    @param mixed $second        Object to compare with.
304     *    @param boolean $identical   If true then type anomolies count.
305     *    @return string              Human readable description.
306     */
307    protected function describeObjectDifference($first, $second, $identical) {
308        if (! is_object($second)) {
309            return $this->describeGenericDifference($first, $second);
310        }
311        return $this->describeArrayDifference(
312                $this->getMembers($first),
313                $this->getMembers($second),
314                $identical);
315    }
316
317    /**
318     *    Get all members of an object including private and protected ones.
319     *    A safer form of casting to an array.
320     *    @param object $object     Object to list members of,
321     *                              including private ones.
322     *    @return array             Names and values in the object.
323     */
324    protected function getMembers($object) {
325        $reflection = new ReflectionObject($object);
326        $members = array();
327        foreach ($reflection->getProperties() as $property) {
328            if (method_exists($property, 'setAccessible')) {
329                $property->setAccessible(true);
330            }
331            try {
332                $members[$property->getName()] = $property->getValue($object);
333            } catch (ReflectionException $e) {
334                $members[$property->getName()] =
335                    $this->getPrivatePropertyNoMatterWhat($property->getName(), $object);
336            }
337        }
338        return $members;
339    }
340
341    /**
342     *    Extracts a private member's value when reflection won't play ball.
343     *    @param string $name        Property name.
344     *    @param object $object      Object to read.
345     *    @return mixed              Value of property.
346     */
347    private function getPrivatePropertyNoMatterWhat($name, $object) {
348        foreach ((array)$object as $mangled_name => $value) {
349            if ($this->unmangle($mangled_name) == $name) {
350                return $value;
351            }
352        }
353    }
354
355    /**
356     *    Removes crud from property name after it's been converted
357     *    to an array.
358     *    @param string $mangled     Name from array cast.
359     *    @return string             Cleaned up name.
360     */
361    function unmangle($mangled) {
362        $parts = preg_split('/[^a-zA-Z0-9_\x7f-\xff]+/', $mangled);
363        return array_pop($parts);
364    }
365
366    /**
367     *    Find the first character position that differs
368     *    in two strings by binary chop.
369     *    @param string $first        First string.
370     *    @param string $second       String to compare with.
371     *    @return integer             Position of first differing
372     *                                character.
373     *    @access private
374     */
375    protected function stringDiffersAt($first, $second) {
376        if (! $first || ! $second) {
377            return 0;
378        }
379        if (strlen($first) < strlen($second)) {
380            list($first, $second) = array($second, $first);
381        }
382        $position = 0;
383        $step = strlen($first);
384        while ($step > 1) {
385            $step = (integer)(($step + 1) / 2);
386            if (strncmp($first, $second, $position + $step) == 0) {
387                $position += $step;
388            }
389        }
390        return $position;
391    }
392
393    /**
394     *    Sends a formatted dump of a variable to a string.
395     *    @param mixed $variable    Variable to display.
396     *    @return string            Output from print_r().
397     *    @access public
398     */
399    function dump($variable) {
400        ob_start();
401        print_r($variable);
402        $formatted = ob_get_contents();
403        ob_end_clean();
404        return $formatted;
405    }
406}
407?>