Twiml.php 4.27 KB
Newer Older
Juliper committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
<?php

/**
 * Exception class for Services_Twilio_Twiml.
 */
class Services_Twilio_TwimlException extends Exception {}

/**
 * Twiml response generator.
 *
 * Author:   Neuman Vong <neuman at ashmoremusic dot com>
 * License:  http://creativecommons.org/licenses/MIT/ MIT
 */
class Services_Twilio_Twiml {

    protected $element;

    /**
     * Constructs a Twiml response.
     *
     * :param SimpleXmlElement|array $arg: Can be any of
     *
     *   - the element to wrap
     *   - attributes to add to the element
     *   - if null, initialize an empty element named 'Response'
     */
    public function __construct($arg = null) {
        switch (true) {
        case $arg instanceof SimpleXmlElement:
            $this->element = $arg;
            break;
        case $arg === null:
            $this->element = new SimpleXmlElement('<Response/>');
            break;
        case is_array($arg):
            $this->element = new SimpleXmlElement('<Response/>');
            foreach ($arg as $name => $value) {
                $this->element->addAttribute($name, $value);
            }
            break;
        default:
            throw new Services_Twilio_TwimlException('Invalid argument');
        }
    }

    /**
     * Converts method calls into Twiml verbs.
     *
     * A basic example:
     *
     * .. code-block:: php
     *
     *     php> print $this->say('hello');
     *     <Say>hello</Say>
     *
     * An example with attributes:
     *
     * .. code-block:: php
     *
     *     print $this->say('hello', array('voice' => 'woman'));
     *     <Say voice="woman">hello</Say>
     *
     * You could even just pass in an attributes array, omitting the noun:
     *
     * .. code-block:: php
     *
     *     print $this->gather(array('timeout' => '20'));
     *     <Gather timeout="20"/>
     *
     * :param string $verb: The Twiml verb.
     * :param array  $args:
     *   - (noun string)
     *   - (noun string, attributes array)
     *   - (attributes array)
     *
     * :return: A SimpleXmlElement
     * :rtype: SimpleXmlElement
     */
    public function __call($verb, array $args)
    {
        list($noun, $attrs) = $args + array('', array());
        if (is_array($noun)) {
            list($attrs, $noun) = array($noun, '');
        }
        /* addChild does not escape XML, while addAttribute does. This means if
         * you pass unescaped ampersands ("&") to addChild, you will generate
         * an error.
         *
         * Some inexperienced developers will pass in unescaped ampersands, and
         * we want to make their code work, by escaping the ampersands for them
         * before passing the string to addChild. (with htmlentities)
         *
         * However other people will know what to do, and their code
         * already escapes ampersands before passing them to addChild. We don't
         * want to break their existing code by turning their &amp;'s into
         * &amp;amp;
         *
         * We also want to use numeric entities, not named entities so that we
         * are fully compatible with XML
         *
         * The following lines accomplish the desired behavior.
         */
        $decoded = html_entity_decode($noun, ENT_COMPAT, 'UTF-8');
        $normalized = htmlspecialchars($decoded, ENT_COMPAT, 'UTF-8', false);
        $child = empty($noun)
            ? $this->element->addChild(ucfirst($verb))
            : $this->element->addChild(ucfirst($verb), $normalized);
        foreach ($attrs as $name => $value) {
            /* Note that addAttribute escapes raw ampersands by default, so we
             * haven't touched its implementation. So this is the matrix for
             * addAttribute:
             *
             * & turns into &amp;
             * &amp; turns into &amp;amp;
             */
            if (is_bool($value)) {
                $value = ($value === true) ? 'true' : 'false';
            }
            $child->addAttribute($name, $value);
        }
        return new static($child);
    }

    /**
     * Returns the object as XML.
     *
     * :return: The response as an XML string
     * :rtype: string
     */
    public function __toString()
    {
        $xml = $this->element->asXml();
        return str_replace(
            '<?xml version="1.0"?>',
            '<?xml version="1.0" encoding="UTF-8"?>', $xml);
    }
}