framework/helpers/BaseFileHelper.php
public static function copyDirectory($src, $dst, $options = [])
    {
        $src = static::normalizePath($src);
        $dst = static::normalizePath($dst);

        if ($src === $dst || strpos($dst, $src . DIRECTORY_SEPARATOR) === 0) {
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
        }
        $dstExists = is_dir($dst);
        if (!$dstExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
            static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
            $dstExists = true;
        }

        $handle = opendir($src);
        if ($handle === false) {
            throw new InvalidArgumentException("Unable to open directory: $src");
        }
        if (!isset($options['basePath'])) {
            // this should be done only once
            $options['basePath'] = realpath($src);
            $options = static::normalizeOptions($options);
        }
        while (($file = readdir($handle)) !== false) {
            if ($file === '.' || $file === '..') {
                continue;
            }
            $from = $src . DIRECTORY_SEPARATOR . $file;
            $to = $dst . DIRECTORY_SEPARATOR . $file;
            if (static::filterPath($from, $options)) {
                if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) {
                    continue;
                }
                if (is_file($from)) {
                    if (!$dstExists) {
                        // delay creation of destination directory until the first file is copied to avoid creating empty directories
                        static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
                        $dstExists = true;
                    }
                    copy($from, $to);
                    if (isset($options['fileMode'])) {
                        @chmod($to, $options['fileMode']);
                    }
                } else {
                    // recursive copy, defaults to true
                    if (!isset($options['recursive']) || $options['recursive']) {
                        static::copyDirectory($from, $to, $options);
                    }
                }
                if (isset($options['afterCopy'])) {
                    call_user_func($options['afterCopy'], $from, $to);
                }
            }
        }
        closedir($handle);
    }
public static function copyDirectory($src, $dst, $options = [])
    {
        $src = static::normalizePath($src);
        $dst = static::normalizePath($dst);

        if ($src === $dst || strpos($dst, $src . DIRECTORY_SEPARATOR) === 0) {
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
        }
        $dstExists = is_dir($dst);
        if (!$dstExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
            static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
            $dstExists = true;
        }

        $handle = opendir($src);
        if ($handle === false) {
            throw new InvalidArgumentException("Unable to open directory: $src");
        }
        if (!isset($options['basePath'])) {
            // this should be done only once
            $options['basePath'] = realpath($src);
            $options = static::normalizeOptions($options);
        }
        while (($file = readdir($handle)) !== false) {
            if ($file === '.' || $file === '..') {
                continue;
            }
            $from = $src . DIRECTORY_SEPARATOR . $file;
            $to = $dst . DIRECTORY_SEPARATOR . $file;
            if (static::filterPath($from, $options)) {
                if (isset($options['beforeCopy']) && !call_user_func($options['beforeCopy'], $from, $to)) {
                    continue;
                }
                if (is_file($from)) {
                    if (!$dstExists) {
                        // delay creation of destination directory until the first file is copied to avoid creating empty directories
                        static::createDirectory($dst, isset($options['dirMode']) ? $options['dirMode'] : 0775, true);
                        $dstExists = true;
                    }
                    copy($from, $to);
                    if (isset($options['fileMode'])) {
                        @chmod($to, $options['fileMode']);
                    }
                } else {
                    // recursive copy, defaults to true
                    if (!isset($options['recursive']) || $options['recursive']) {
                        static::copyDirectory($from, $to, $options);
                    }
                }
                if (isset($options['afterCopy'])) {
                    call_user_func($options['afterCopy'], $from, $to);
                }
            }
        }
        closedir($handle);
    }
Copies a whole directory as another one.
The files and sub-directories will also be copied over.
@param string $src the source directory
@param string $dst the destination directory
@param array $options options for directory copy. Valid options are:

- dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
- fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment setting.
- filter: callback, a PHP callback that is called for each directory or file.
The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
The callback can return one of the following values:

* true: the directory or file will be copied (the "only" and "except" options will be ignored)
* false: the directory or file will NOT be copied (the "only" and "except" options will be ignored)
* null: the "only" and "except" options will determine whether the directory or file should be copied

- only: array, list of patterns that the file paths should match if they want to be copied.
A path matches a pattern if it contains the pattern string at its end.
For example, '.php' matches all file paths ending with '.php'.
Note, the '/' characters in a pattern matches both '/' and '\' in the paths.
If a file path matches a pattern in both "only" and "except", it will NOT be copied.
- except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
A path matches a pattern if it contains the pattern string at its end.
Patterns ending with '/' apply to directory paths only, and patterns not ending with '/'
apply to file paths only. For example, '/a/b' matches all file paths ending with '/a/b';
and '.svn/' matches directory paths ending with '.svn'. Note, the '/' characters in a pattern matches
both '/' and '\' in the paths.
- caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
- recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
- beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
If the callback returns false, the copy operation for the sub-directory or file will be cancelled.
The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
file to be copied from, while `$to` is the copy target.
- afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
file copied from, while `$to` is the copy target.
- copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating directories
that do not contain files. This affects directories that do not contain files initially as well as directories that
do not contain files at the target destination because files have been filtered via `only` or `except`.
Defaults to true. This option is available since version 2.0.12. Before 2.0.12 empty directories are always copied.
@throws InvalidArgumentException if unable to open directory