PHPからFTPをたたこうとすると、「中身入りのディレクトリを削除(or空にする)」処理と「サブディレクトリひっくるめてアップロード」が無いという、ホントFTPコマンド投げてるだけかよっちゅうアレでもう、ね。

とりあえず、コネクションの管理ひっくるめて全部マネージャクラスに押し込めましたよ、ええ。

<?php
class ftpManager {
    const FTPERR_CANT_CONNECT = -1; // FTPサーバへの接続に失敗
    const FTPERR_LOGIN_FAILD = -2;  // FTPサーバへのログインに失敗
    const FTPERR_SUCCESS_BUT_DO_NOT_CHANGE_DIRECTORY = -3; // 接続とログインに成功したが、設定ファイルのdirectoryに移動できなかった

    private $_connected = false;
    private $_connect_id = null;
    private $_ftpconfig = null;

    /**
     * 生成子
     *
     * @param array $config application.iniの外部サーバ設定セクション
     */
    public function __construct($config)
    {
        $this->_ftpconfig = new Zend_Config_xml($config['path'], $config['ftp']['configsection']);
    }

    function __destruct() {
       // デストラクタを呼ばれる保証はない。disconnectされない場合、サーバ側からタイムアウトで切断される。
        $this->disconnect();
    }

    /**
     * 接続済みフラグプロパティ
     *
     */
    public function getConnected()
    {
        return $this->_connected;
    }

    /**
     * コネクションIDプロパティ
     *
     */
    public function getConnectID()
    {
        return $this->_connect_id;
    }

    /**
     * FTP接続実行
     *
     * @return boolean/integer
     *   true: 接続とログイン成功
     *   FTPERR_CANT_CONNECT: 接続に失敗
     *   FTPERR_LOGIN_FAILD : 接続は出来たがログインに失敗
     *   FTPERR_SUCCESS_BUT_DO_NOT_CHANGE_DIRECTORY : 接続とログインには成功したが、ftp設定情報のdirectoryに移動できなかった。
     *                                                (このエラーが返った時は、接続自体は生きている)
     */
    public function connect()
    {
        $this->_connected = false;
        $this->_connect_id = ftp_connect($this->_ftpconfig->host);
        if ($this->_connect_id == false) {
            $this->_connect_id = null;
            return self::FTPERR_CANT_CONNECT;
        } else {
            if (ftp_login($this->_connect_id, $this->_ftpconfig->username, $this->_ftpconfig->password)) {
                $this->_connected = true;
                if (ftp_chdir($this->_connect_id, $this->_ftpconfig->directory)) {
                    return true;
                } else {
                    return self::FTPERR_SUCCESS_BUT_DO_NOT_CHANGE_DIRECTORY;
                }
            } else {
                $this->disconnect();
                return self::FTPERR_LOGIN_FAILD;
            }
        }
    }

    /**
     * FTP接続を切断
     *
     */
    public function disconnect()
    {
        if (!is_null($this->_connect_id)) {
            ftp_close($this->_connect_id);
            $this->_connect_id = null;
        }
        $this->_connected = false;
    }

    /**
     * 指定したsource_pathにあるすべてのファイルとフォルダとサブフォルダをdest_pathにアップロードする。
     *  dest_path内のクリーニングはここではやらない。必要なら呼び出す前にやっておくこと。
     *
     * @param unknown_type $source_path
     * @param unknown_type $dest_path
     * @return boolean
     */
    public function uploadpath($source_path, $dest_path, $logging = false)
    {
        if ($this->_connected == false) {
            return false;
        }

        $con_id = $this->_connect_id;
        $current_dir = ftp_pwd($con_id);
        if (ftp_chdir($con_id, $dest_path) == false) {
            return false;
        }

        // ftpサーバ側の現在パスをアップロードのルートパスにしてからbodyを呼び出す
        // 初回の呼び出し時は$dest_path=''にする
        $this->uploadpath_body($con_id, $source_path, '', $logging);

        ftp_chdir($con_id, $current_dir);

        return true;
    }

    // ファイルアップロード本体
    private function uploadpath_body($con_id, $source_path, $dest_path, $logging)
    {
        $current_dir = ftp_pwd($con_id);

        if ($dest_path <> '') {
            if (ftp_chdir($con_id, $dest_path) == false) {
                return false;
            }
        }

        if ($handle = opendir($source_path)) {
            while (false !== ($entry = readdir($handle))) {
                // カレントとパレントは飛ばす
                if (($entry == '.') || ($entry == '..')) {
                    continue;
                }

                $filename = PathHelper::Combine($source_path, $entry);
                if (is_dir($filename)) {
                    // 検索結果がディレクトリの場合、dest_pathに同じ名前のディレクトリを作成し、再帰処理する
                    $newDir = PathHelper::Combine($dest_path, $entry);
                    if (ftp_mkdir($con_id, $newDir)) {
                        $this->uploadpath_body($con_id, $filename, $newDir, $logging);
                    }
                } else {
                    ftp_put($con_id, $entry, $filename, FTP_BINARY));
                }
            }
            closedir($handle);
        }
        ftp_chdir($con_id, $current_dir);
    }


    /**
     * 指定したディレクトリを空にする(public呼び出し用)
     *
     * @param string $directory
     * @return boolean
     */
    public function force_clear($directory)
    {
        if ($this->_connected == false) {
            return false;
        }

        return $this->ftp_force_clear($this->_connect_id, $directory);
    }

    // 指定したftpディレクトリを空にする-処理本体
    private function ftp_force_clear($con_id, $directory)
    {
        // 現在のパスを保存しておいて、フォルダを戻す時にここに戻す
        $current_dir = ftp_pwd($con_id);
        $file_list = ftp_nlist($con_id, $directory);
        foreach ($file_list as $filename) {
            // warningを出したくないので@で抑制している。
            // ftp_deleteで削除出来たらファイルだった
            if (@ftp_delete($con_id, $filename) == true) {
                continue;
            }

            // ftp_rmdirを試してみる。削除出来たら続行。
            if (@ftp_rmdir($con_id, $filename) == true) {
                continue;
            }

            // ftp_rmdirで削除出来なかったら、その名前のフォルダに移動してみる
            // 移動できなかったら、$filenameがファイルでかつ削除失敗しているのでfalseを返す
            if (@ftp_chdir($con_id, $filename) == false) {
                return false;
            }

            // フォルダを戻す。失敗したら副作用が怖いのでfalseを戻して終了
            if (@ftp_chdir($con_id, $current_dir) == false) {
                return false;
            }

            // 消せなかったフォルダに対して再帰処理
            // falseが戻ってきたらfalseを戻して終了
            if ($this->ftp_force_clear($con_id, $filename) == false) {
                return false;
            }

            // $filenameフォルダは空のはずなので削除。削除に失敗したらfalseを戻す
            if (@ftp_rmdir($con_id, $filename) == false) {
                return false;
            }
            // 削除に成功したら続行
        }
        // ここまで来たらディレクトリ内部が空になっているハズ
        return true;
    }
}

設定定義ファイルについては詳細はヒミツです。
Zend Frameworkのapplication.iniと、外部サーバの設定が書かれたxmlファイルがある、ということで。


後は、connect()してから実際のファイル操作までにタイムアウトした場合、クラス内部からソレを察知する手段が無いのがちと困る。
まぁ、システム内で実際に使う時は、connectから転送からdisconnectまで一気にやるので、そこまで手間かけて実装する意味ないからいいですケド。