1
\$\begingroup\$

I wrote a wrapper class in PHP to use Telegram bot API. What do you think about it?

I used this class in some projects and it doesn't have any problem but I want to know if it is optimized?

<?php
/**
 * A wrapper class for telegram bot api
 * you can find source code in https://github.com/smoqadam/php-telegram-bot
 * 
 * @author : Saeed Moqadam <[email protected]>
 * @licence : MIT Licence
 *
 */
namespace Smoqadam;


class Telegram
{


    const ACTION_TYPING = 'typing';
    const ACTION_UPLOAD_PHOTO = 'upload_photo';
    const ACTION_RECORD_VIDEO = 'record_video';
    const ACTION_UPLOAD_VIDEO = 'upload_video';
    const ACTION_RECORD_AUDIO = 'record_audio';
    const ACTION_UPLOAD_AUDIO = 'upload_audio';
    const ACTION_UPLOAD_DOC = 'upload_document';
    const ACTION_FIND_LOCATION = 'find_location';


    public $api = 'https://api.telegram.org/bot';

    /**
     * returned json from telegram api parse to object and save to result
     * @var
     */
    public $result;

    /**
     * commands in regex
     * @var array
     */
    private $commands = [];

    /**
     * callbacks for commands
     * @var array
     */
    private $callbacks = [];

    /**
     * available telegram bot commands
     * @var array
     */
    private $available_commands = [
        'getMe',
        'sendMessage',
        'forwardMessage',
        'sendPhoto',
        'sendAudio',
        'sendDocument',
        'sendSticker',
        'sendVideo',
        'sendLocation',
        'sendChatAction',
        'getUserProfilePhotos',
        'getUpdates',
        'setWebhook',
    ];

    /**
     * pre patterns you can use in regex
     * @var array
     */
    private $patterns = [
        ':any' => '.*',
        ':num' => '[0-9]{0,}',
        ':str' => '[a-zA-z]{0,}',
    ];

    /**
     *
     * @param String $token Telegram api token , taken by botfather
     */
    public function __construct($token)
    {

        $this->api .= $token;

    }


    /**
     * add new command to the bot
     * @param String $cmd
     * @param \Closure $func
     */
    public function cmd($cmd, $func)
    {
        $this->commands[] = $cmd;
        $this->callbacks[] = $func;
    }


    /**
     * this method check for recived message(command) and then execute the
     * command function
     *
     * @param bool $sleep
     */
    public function run($sleep = false)
    {

        $result = $this->getUpdates();
        while (true) {

            $update_id = isset($result->update_id) ? $result->update_id : 1;
            $result = $this->getUpdates($update_id + 1);

            $this->processMessage($result);

            if ($sleep !== false)
                sleep($sleep);
        }
    }

    /**
    * this method used for setWebhook sended message
    */
    public function process($message)
    {
            $result = $this->convertToObject($message, true);

            $update_id = isset($result->update_id) ? $result->update_id : 1;

            return $this->processMessage($result);


    }


    private function processMessage($result)
    {
        if ($result) {
                try {
                    $this->result = $result;

                    // message recived by user
                    $recived_command = $this->result->message->text;

                    $args = null;

                    $pos = 0;
                    foreach ($this->commands as $pattern) {

                        // replace public patterns to regex pattern
                        $searchs = array_keys($this->patterns);
                        $replaces = array_values($this->patterns);
                        $pattern = str_replace($searchs, $replaces, $pattern);

                        //find args pattern
                        $args = $this->getArgs($pattern, $recived_command);

                        $pattern = '/^' . $pattern . '/i';

                        preg_match($pattern, $recived_command, $matches);

                        if (isset($matches[0])) {

                            $func = $this->callbacks[$pos];

                            call_user_func($func, $args);

                        }

                        $pos++;

                    }
                } catch (\Exception $e) {
                    echo "\r\n Exception :: " . $e->getMessage();
                }
            } else {
                echo "\r\nNo new message\r\n";
            }
    }
    /**
     * get arguments part in regex
     * @param $pattern
     * @param $recived_command
     * @return mixed|null
     */
    private function getArgs(&$pattern, $recived_command)
    {

        $args = null;
        // if command has argument
        if (preg_match('/<<.*>>/', $pattern, $matches)) {

            $args_pattern = $matches[0];
            //remove << and >> from patterns

            $tmp_args_pattern = str_replace(['<<', '>>'], ['(', ')'], $pattern);

            //if args set
            if (preg_match('/' . $tmp_args_pattern . '/i', $recived_command, $matches)) {
                //remove first element
                array_shift($matches);

                if (isset($matches[0])) {

                    //set args
                    $args = array_shift($matches);

                    //remove args pattern from main pattern
                    $pattern = str_replace($args_pattern, '', $pattern);

                }
            }
        }
        return $args;
    }


    /**
     * execute telegram api commands
     * @param $command
     * @param array $params
     */
    private function exec($command, $params = [])
    {
        if (in_array($command, $this->available_commands)) {


            // convert json to array then get the last messages info
            $output = json_decode($this->curl_get_contents($this->api . '/' . $command, $params), true);
            return $this->convertToObject($output);


        } else {

            echo 'command not found';

        }
    }


    private function convertToObject($jsonObject , $webhook = false)
    {

        if(!$webhook){
          if ($jsonObject['ok']) {

                // remove unwanted array elements
                $output = end($jsonObject);

                $result = is_array($output) ? end($output) : $output;

                if (!empty($result)) {
                    // convert to object
                    return json_decode(json_encode($result));
                }
            }

        }else{
            if ($jsonObject['message']) {

                    return json_decode(json_encode($jsonObject)); 
                // remove unwanted array elements
                $output = end($jsonObject);

                $result = is_array($output) ? end($output) : $output;

                if (!empty($result)) {
                    // convert to object
                    print_r($result);
                    return json_decode(json_encode($result));
                }
            }
        }


    }

    /**
     * get the $url content with CURL
     * @param $url
     * @param $params
     * @return mixed
     */
    private function curl_get_contents($url, $params)
    {

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

        curl_setopt($ch, CURLOPT_POST, count($params));

        curl_setopt($ch, CURLOPT_POSTFIELDS, $params);

        $result = curl_exec($ch);

        curl_close($ch);

        return $result;

    }

    /**
     * Get current chat id
     * @param null $chat_id
     * @return int
     */
    public function getChatId($chat_id = null)
    {
        if ($chat_id)
            return $chat_id;

        return $this->result->message->chat->id;
    }

    /**
     * @param null $offset
     * @param int $limit
     * @param int $timeout
     */
    public function getUpdates($offset = null, $limit = 1, $timeout = 1)
    {
        return $this->exec('getUpdates', [
            'offset' => $offset,
            'limit' => $limit,
            'timeout' => $timeout
        ]);
    }


    /**
     * send message
     * @param $text
     * @param $chat_id
     * @param bool $disable_web_page_preview
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendMessage($text, $chat_id, $disable_web_page_preview = false, $reply_to_message_id = null, $reply_markup = null)
    {

        $this->sendChatAction(self::ACTION_TYPING,$chat_id);
        return $this->exec('sendMessage', [
            'chat_id' => $this->getChatId($chat_id),
            'text' => $text,
            'disable_web_page_preview' => $disable_web_page_preview,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);
    }


    /**
     * Get me
     */
    public function getMe()
    {
        return $this->exec('getMe');
    }


    /**
     * @param $from_id
     * @param $message_id
     * @param null $chat_id
     */
    public function forwardMessage($from_id, $message_id, $chat_id = null)
    {
        return $this->exec('forwardMessage', [
            'chat_id' => $this->getChatId($chat_id),
            'from_chat_id' => $from_id,
            'message_id' => $message_id,
        ]);
    }

    /**
     * @param $photo photo file patch
     * @param null $chat_id
     * @param null $caption
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendPhoto($photo, $chat_id = null, $caption = null, $reply_to_message_id = null, $reply_markup = null)
    {

        $res = $this->exec('sendPhoto', [
            'chat_id' => $this->getChatId($chat_id),
            'photo' => '@' . $photo,
            'caption' => $caption,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;

    }

    /**
     * @param $video video file path
     * @param null $chat_id
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendVideo($video, $chat_id = null, $reply_to_message_id = null, $reply_markup = null)
    {
        $res = $this->exec('sendVideo', [
            'chat_id' => $this->getChatId($chat_id),
            'video' => '@' . $video,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;
    }

    /**
     *
     * @param $sticker
     * @param null $chat_id
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendSticker($sticker, $chat_id = null, $reply_to_message_id = null, $reply_markup = null)
    {
        $res = $this->exec('sendSticker', [
            'chat_id' => $this->getChatId($chat_id),
            'sticker' => '@' . $sticker,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;
        // as soons as possible
    }

    /**
     * @param $latitude
     * @param $longitude
     * @param null $chat_id
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendLocation($latitude, $longitude, $chat_id = null, $reply_to_message_id = null, $reply_markup = null)
    {
        $res = $this->exec('sendLocation', [
            'chat_id' => $this->getChatId($chat_id),
            'latitude' => $latitude,
            'longitude' => $longitude,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;
    }

    /**
     * @param $document
     * @param null $chat_id
     * @param null $reply_to_message_id
     * @param null $reply_markup
     */
    public function sendDocument($document, $chat_id = null, $reply_to_message_id = null, $reply_markup = null)
    {
        $res = $this->exec('sendDocument', [
            'chat_id' => $this->getChatId($chat_id),
            'document' => '@' . $document,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;
    }

    public function sendAudio($audio, $chat_id = null, $reply_to_message_id = null, $reply_markup = null)
    {
        $res = $this->exec('sendAudio', [
            'chat_id' => $this->getChatId($chat_id),
            'audio' => '@' . $audio,
            'reply_to_message_id' => $reply_to_message_id,
            'reply_markup' => $reply_markup
        ]);

        return $res;
    }

    /**
     * send chat action : Telegram::ACTION_TYPING , ...
     * @param $action
     * @param null $chat_id
     */
    public function sendChatAction($action, $chat_id = null)
    {
        $res = $this->exec('sendChatAction', [
            'chat_id' => $this->getChatId($chat_id),
            'action' => $action
        ]);

        return $res;
    }

    /**
     * @param $user_id
     * @param null $offset
     * @param null $limit
     */
    public function getUserProfilePhotos($user_id, $offset = null, $limit = null)
    {
        $res = $this->exec('getUserProfilePhotos', [
            'user_id' => $user_id,
            'offset' => $offset,
            'limit' => $limit
        ]);

        return $res;
    }

    /**
     * @param $url
     */
    public function setWebhook($url)
    {
        $res = $this->exec('setWebhook', [
            'url' => $url
        ]);

        return $res;
    }


}

You can use it in this way:

$tg = new Telegram($api_token);
$tg->cmd('Hi <<:any>>', function ($arg)use ($tg) {
    $tg->sendMessage("Hello $arg",$tg->getChatId());
});
// in cli you must call run() method
$tg->run();

// if you use webhook you have to use $tg->process($message);
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$
  • [0-9]{0,} is more simply written as \d*

  • [a-zA-z]{0,} is more simply/accurately written as [a-z]* because you are using a case-insensitive pattern in $pattern later in the script.

  • You may want to validate/sanitize $token before doing $this->api .= $token;. If you are expecting an alphanumeric string, then maybe ctype_alnum(). I don't know the validation rules of the token, but I don't think I like that the url is being appended with that string without being vetted.

  • After a quick glance of run() I think you probably want to use a do{}while() loop so that you can avoid writing getUpdates() twice in your code.

  • $update_id = isset($result->update_id) ? $result->update_id : 1; could be rewritten with the null coalescing operator as $update_id = $result->update_id ?? 1; but I'm not sure this part is working as designed. Is it supposed to keep incrementing in that loop? I feel like it is going to get stuck repeating on the first + 1.

  • Always use curly braces to mark the body of a condition block. Too many times have scripts had bugs because the developer tried to save a line of code. Just use:

    if ($sleep !== false) {
        sleep($sleep);
    }
    
  • In process(), you have a pointless/ineffectual line of code:

    $update_id = isset($result->update_id) ? $result->update_id : 1;
    

    This overwriting of $update_id is useless because this variable is never again accessed in the method. This raises my concern that your while(true) loop is not doing what you expect it to do. I think you actually mean to do something with $result->update_id.

  • $replaces = array_values($this->patterns); can be simplified to $replaces = $this->patterns; because str_replace() will ignore the keys anyhow. I find the act of calling str_replace() to create $pattern to be strange. I mean it will potentially replace replacements. Perhaps strtr() would be more ideal (it happily consumes an associative array anyhow)? I don't know. I am not understanding exactly how this part is working in this context.

  • I think I would re-shuffle some of the logic following //find args pattern to only define $args when necessary and avoid isset($matches[0]). Normally I advise against single-use variables, but in this case, maybe there is improved code comprehension by declaring the variables.

    if (preg_match('/^' . $pattern . '/i', $recived_command, $matches)) {
        $func = $this->callbacks[$pos];
        $args = $this->getArgs($pattern, $recived_command);
        call_user_func($func, $args);
    }
    
  • I think I need some sample data and expected output to review getArgs(). Overall, I find the code logic to be difficult to follow or indirect (despite the inline comments)

  • I think exec() should probably throw an exception if the command is not in the whitelist of available commands. echo is probably not enough.

  • convertToObject() doesn't look very D.R.Y., this part is probably due for a clean up / refactor.

  • As a "cute" way to condense an array of associative elements which contain a variable by the same name as the key, use compact().

    compact(['offset', 'limit', 'timeout'])
    
  • I don't think it is valuable to declare $res and then return $res immediately after; just return the data and remove the single-use variable declaration.

  • Finally, I recommend declaring return types to help you and your IDE to keep your data clean and correct.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.