gielfeldt/iterators

View on GitHub
src/GlobIterator.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Gielfeldt\Iterators;

/**
 * Glob iterator with double wildcard (**) and recursive capabilities.
 */
class GlobIterator extends TraversableIterator implements \Countable
{
    const GLOB_NOSORT = 2048;
    const GLOB_ONLYDIR = 16384;

    protected $flags;
    protected $path;

    public function getPath()
    {
        return $this->path;
    }

    /**
     * Constructor.
     *
     * @param string  $globPattern
     *   Glob pattern.
     * @param integer $flags
     *   FilesystemIterator flags.
     */
    public function __construct(string $globPattern, int $flags = self::GLOB_NOSORT | \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO)
    {
        $this->flags = $flags;
        list($path, $maxDepth) = self::extractPathAndMaxDepth($globPattern);
        $regexPattern = self::globToRegex($globPattern);

        $this->path = $path;

        $realPath = $path ? $path : './';
        $realPath = rtrim($realPath, '/') . '/';

        $flags = $flags & ~ \FilesystemIterator::CURRENT_AS_PATHNAME;
        $flags |= \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS;
        $iterator = new \RecursiveDirectoryIterator($realPath, $flags);

        // Sort if necessary.
        $sIterator = $this->flags & self::GLOB_NOSORT ? $iterator : new RecursiveSortIterator(
            $iterator,
            RecursiveSortIterator::SORT_ASC,
            0,
            [$this, 'sortSplFileInfo']
        );

        // Only traverse the depth needed.
        $rIterator = new \RecursiveIteratorIterator($sIterator, \RecursiveIteratorIterator::SELF_FIRST);
        $rIterator->setMaxDepth($maxDepth);

        // Setup file info handler.
        $iteratorId = spl_object_hash($this);
        GlobIteratorFileInfo::setPath($iteratorId, $path, $realPath);

        // Actual glob filtering.
        $fIterator = new \CallbackFilterIterator(
            $rIterator,
            function (&$current, &$key) use ($iteratorId, $regexPattern) {
                if (($this->flags & self::GLOB_ONLYDIR) && !$current->isDir()) {
                    return false;
                }
                GlobIteratorFileInfo::setIteratorId($iteratorId);
                $fileInfo = $current->getFileInfo(GlobIteratorFileInfo::class);
                if ($this->flags & \FilesystemIterator::CURRENT_AS_PATHNAME) {
                    $current = $fileInfo->getPathname();
                } else {
                    $current = $fileInfo;
                }

                if ($this->flags & \FilesystemIterator::KEY_AS_FILENAME) {
                    $key = $fileInfo->getFilename();
                } else {
                    $key = $fileInfo->getPathname();
                }
                return preg_match($regexPattern, $fileInfo->getPathname());
            }
        );
        parent::__construct(new CountableIterator($fIterator, CountableIterator::CACHE_COUNT));
    }

    public function count()
    {
        return $this->getInnerIterator()->count();
    }

    public function sortSplFileInfo($cmpA, $cmpB)
    {
        return $cmpA->current->getPathname() <=> $cmpB->current->getPathname();
    }

    /**
     * Extract the path to start at.
     *
     * @param  string $globPattern
     * @return string
     */
    public static function extractPathAndMaxDepth(string $globPattern): array
    {
        if (strpos($globPattern, '*')) {
            list($path,) = explode('*', $globPattern, 2);
        }
        $path = isset($path) ? dirname("$path.") . '/' : '';
        $subPattern = substr($globPattern, strlen($path));
        $maxDepth = strpos($globPattern, '**') !== false ? -1 : substr_count($subPattern, '/');
        return [$path, $maxDepth];
    }

    /**
     * Convert a glob pattern to a regex pattern.
     *
     * @param  string $globPattern
     * @return string
     */
    public static function globToRegex(string $globPattern): string
    {
        $modifiers = '';
        $transforms = array(
            '\*'    => '[^/]*',
            '\*\*'    => '.*',
            '\?'    => '[^/]',
            '\[\!'    => '[^',
            '\['    => '[',
            '\]'    => ']',
            '\.'    => '\.',
        );
        $regexPattern = '#^'
            . strtr(preg_quote($globPattern, '#'), $transforms)
            . '$#'
            . $modifiers;

        return $regexPattern;
    }

    /**
     * Use the RecursiveGlobIterator for glob://
     */
    public static function registerStreamWrapper($root = null)
    {
        stream_wrapper_unregister('glob');
        stream_wrapper_register('glob', GlobStreamWrapper::class);
        GlobStreamWrapper::setRoot($root);
    }

    /**
     * Restore the default glob:// stream wrapper.
     */
    public static function unRegisterStreamWrapper()
    {
        stream_wrapper_unregister('glob');
        stream_wrapper_restore('glob');
        GlobStreamWrapper::setRoot(null);
    }
}