# packeta/trunk/deps/nette/robot-loader/src/RobotLoader/RobotLoader.php

Packeta, version trunk. 450 lines.

- Page: https://pluginprobe.com/plugins/packeta/trunk/code/deps/nette/robot-loader/src/RobotLoader/RobotLoader.php
- Raw: https://pluginprobe.com/plugins/packeta/trunk/raw/deps/nette/robot-loader/src/RobotLoader/RobotLoader.php
- Modified: 2025-03-10T10:47:38+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/packeta/trunk/code/deps/nette/robot-loader/src/RobotLoader/RobotLoader.php#L10-L20`.

```php
<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */
declare (strict_types=1);
namespace Packetery\Nette\Loaders;

use Packetery\Nette;
use SplFileInfo;
/**
 * Nette auto loader is responsible for loading classes and interfaces.
 *
 * <code>
 * $loader = new \Packetery\Nette\Loaders\RobotLoader;
 * $loader->addDirectory('app');
 * $loader->excludeDirectory('app/exclude');
 * $loader->setTempDirectory('temp');
 * $loader->register();
 * </code>
 */
class RobotLoader
{
    use \Packetery\Nette\SmartObject;
    private const RETRY_LIMIT = 3;
    /** @var string[] */
    public $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp'];
    /** @var string[] */
    public $acceptFiles = ['*.php'];
    /** @var bool */
    private $autoRebuild = \true;
    /** @var bool */
    private $reportParseErrors = \true;
    /** @var string[] */
    private $scanPaths = [];
    /** @var string[] */
    private $excludeDirs = [];
    /** @var array<string, array{string, int}>  class => [file, time] */
    private $classes = [];
    /** @var bool */
    private $cacheLoaded = \false;
    /** @var bool */
    private $refreshed = \false;
    /** @var array<string, int>  class => counter */
    private $missingClasses = [];
    /** @var array<string, int>  file => mtime */
    private $emptyFiles = [];
    /** @var string|null */
    private $tempDirectory;
    /** @var bool */
    private $needSave = \false;
    public function __construct()
    {
        if (!\extension_loaded('tokenizer')) {
            throw new \Packetery\Nette\NotSupportedException('PHP extension Tokenizer is not loaded.');
        }
    }
    public function __destruct()
    {
        if ($this->needSave) {
            $this->saveCache();
        }
    }
    /**
     * Register autoloader.
     */
    public function register(bool $prepend = \false) : self
    {
        \spl_autoload_register([$this, 'tryLoad'], \true, $prepend);
        return $this;
    }
    /**
     * Handles autoloading of classes, interfaces or traits.
     */
    public function tryLoad(string $type) : void
    {
        $this->loadCache();
        $missing = $this->missingClasses[$type] ?? null;
        if ($missing >= self::RETRY_LIMIT) {
            return;
        }
        [$file, $mtime] = $this->classes[$type] ?? null;
        if ($this->autoRebuild) {
            if (!$this->refreshed) {
                if (!$file || !\is_file($file)) {
                    $this->refreshClasses();
                    [$file] = $this->classes[$type] ?? null;
                    $this->needSave = \true;
                } elseif (\filemtime($file) !== $mtime) {
                    $this->updateFile($file);
                    [$file] = $this->classes[$type] ?? null;
                    $this->needSave = \true;
                }
            }
            if (!$file || !\is_file($file)) {
                $this->missingClasses[$type] = ++$missing;
                $this->needSave = $this->needSave || $file || $missing <= self::RETRY_LIMIT;
                unset($this->classes[$type]);
                $file = null;
            }
        }
        if ($file) {
            (static function ($file) {
                require $file;
            })($file);
        }
    }
    /**
     * Add path or paths to list.
     * @param  string  ...$paths  absolute path
     */
    public function addDirectory(...$paths) : self
    {
        if (\is_array($paths[0] ?? null)) {
            \trigger_error(__METHOD__ . '() use variadics ...$paths to add an array of paths.', \E_USER_WARNING);
            $paths = $paths[0];
        }
        $this->scanPaths = \array_merge($this->scanPaths, $paths);
        return $this;
    }
    public function reportParseErrors(bool $on = \true) : self
    {
        $this->reportParseErrors = $on;
        return $this;
    }
    /**
     * Excludes path or paths from list.
     * @param  string  ...$paths  absolute path
     */
    public function excludeDirectory(...$paths) : self
    {
        if (\is_array($paths[0] ?? null)) {
            \trigger_error(__METHOD__ . '() use variadics ...$paths to add an array of paths.', \E_USER_WARNING);
            $paths = $paths[0];
        }
        $this->excludeDirs = \array_merge($this->excludeDirs, $paths);
        return $this;
    }
    /**
     * @return array<string, string>  class => filename
     */
    public function getIndexedClasses() : array
    {
        $this->loadCache();
        $res = [];
        foreach ($this->classes as $class => [$file]) {
            $res[$class] = $file;
        }
        return $res;
    }
    /**
     * Rebuilds class list cache.
     */
    public function rebuild() : void
    {
        $this->cacheLoaded = \true;
        $this->classes = $this->missingClasses = $this->emptyFiles = [];
        $this->refreshClasses();
        if ($this->tempDirectory) {
            $this->saveCache();
        }
    }
    /**
     * Refreshes class list cache.
     */
    public function refresh() : void
    {
        $this->loadCache();
        if (!$this->refreshed) {
            $this->refreshClasses();
            $this->saveCache();
        }
    }
    /**
     * Refreshes $this->classes & $this->emptyFiles.
     */
    private function refreshClasses() : void
    {
        $this->refreshed = \true;
        // prevents calling refreshClasses() or updateFile() in tryLoad()
        $files = $this->emptyFiles;
        $classes = [];
        foreach ($this->classes as $class => [$file, $mtime]) {
            $files[$file] = $mtime;
            $classes[$file][] = $class;
        }
        $this->classes = $this->emptyFiles = [];
        foreach ($this->scanPaths as $path) {
            $iterator = \is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path);
            foreach ($iterator as $fileInfo) {
                $mtime = $fileInfo->getMTime();
                $file = $fileInfo->getPathname();
                $foundClasses = isset($files[$file]) && $files[$file] === $mtime ? $classes[$file] ?? [] : $this->scanPhp($file);
                if (!$foundClasses) {
                    $this->emptyFiles[$file] = $mtime;
                }
                $files[$file] = $mtime;
                $classes[$file] = [];
                // prevents the error when adding the same file twice
                foreach ($foundClasses as $class) {
                    if (isset($this->classes[$class])) {
                        throw new \Packetery\Nette\InvalidStateException("Ambiguous class {$class} resolution; defined in {$this->classes[$class][0]} and in {$file}.");
                    }
                    $this->classes[$class] = [$file, $mtime];
                    unset($this->missingClasses[$class]);
                }
            }
        }
    }
    /**
     * Creates an iterator scaning directory for PHP files, subdirectories and 'netterobots.txt' files.
     * @throws \Packetery\Nette\IOException if path is not found
     */
    private function createFileIterator(string $dir) : \Packetery\Nette\Utils\Finder
    {
        if (!\is_dir($dir)) {
            throw new \Packetery\Nette\IOException("File or directory '{$dir}' not found.");
        }
        $dir = \realpath($dir) ?: $dir;
        // realpath does not work in phar
        if (\is_string($ignoreDirs = $this->ignoreDirs)) {
            \trigger_error(self::class . ': $ignoreDirs must be an array.', \E_USER_WARNING);
            $ignoreDirs = \preg_split('#[,\\s]+#', $ignoreDirs);
        }
        $disallow = [];
        foreach (\array_merge($ignoreDirs, $this->excludeDirs) as $item) {
            if ($item = \realpath($item)) {
                $disallow[\str_replace('\\', '/', $item)] = \true;
            }
        }
        if (\is_string($acceptFiles = $this->acceptFiles)) {
            \trigger_error(self::class . ': $acceptFiles must be an array.', \E_USER_WARNING);
            $acceptFiles = \preg_split('#[,\\s]+#', $acceptFiles);
        }
        $iterator = \Packetery\Nette\Utils\Finder::findFiles($acceptFiles)->filter(function (SplFileInfo $file) use(&$disallow) {
            return $file->getRealPath() === \false ? \true : !isset($disallow[\str_replace('\\', '/', $file->getRealPath())]);
        })->from($dir)->exclude($ignoreDirs)->filter($filter = function (SplFileInfo $dir) use(&$disallow) {
            if ($dir->getRealPath() === \false) {
                return \true;
            }
            $path = \str_replace('\\', '/', $dir->getRealPath());
            if (\is_file("{$path}/netterobots.txt")) {
                foreach (\file("{$path}/netterobots.txt") as $s) {
                    if (\preg_match('#^(?:disallow\\s*:)?\\s*(\\S+)#i', $s, $matches)) {
                        $disallow[$path . \rtrim('/' . \ltrim($matches[1], '/'), '/')] = \true;
                    }
                }
            }
            return !isset($disallow[$path]);
        });
        $filter(new SplFileInfo($dir));
        return $iterator;
    }
    private function updateFile(string $file) : void
    {
        foreach ($this->classes as $class => [$prevFile]) {
            if ($file === $prevFile) {
                unset($this->classes[$class]);
            }
        }
        $foundClasses = \is_file($file) ? $this->scanPhp($file) : [];
        foreach ($foundClasses as $class) {
            [$prevFile, $prevMtime] = $this->classes[$class] ?? null;
            if (isset($prevFile) && @\filemtime($prevFile) !== $prevMtime) {
                // @ file may not exists
                $this->updateFile($prevFile);
                [$prevFile] = $this->classes[$class] ?? null;
            }
            if (isset($prevFile)) {
                throw new \Packetery\Nette\InvalidStateException("Ambiguous class {$class} resolution; defined in {$prevFile} and in {$file}.");
            }
            $this->classes[$class] = [$file, \filemtime($file)];
        }
    }
    /**
     * Searches classes, interfaces and traits in PHP file.
     * @return string[]
     */
    private function scanPhp(string $file) : array
    {
        $code = \file_get_contents($file);
        $expected = \false;
        $namespace = $name = '';
        $level = $minLevel = 0;
        $classes = [];
        try {
            $tokens = \token_get_all($code, \TOKEN_PARSE);
        } catch (\ParseError $e) {
            if ($this->reportParseErrors) {
                $rp = new \ReflectionProperty($e, 'file');
                $rp->setAccessible(\true);
                $rp->setValue($e, $file);
                throw $e;
            }
            $tokens = [];
        }
        foreach ($tokens as $token) {
            if (\is_array($token)) {
                switch ($token[0]) {
                    case \T_COMMENT:
                    case \T_DOC_COMMENT:
                    case \T_WHITESPACE:
                        continue 2;
                    case \T_STRING:
                    case \PHP_VERSION_ID < 80000 ? \T_NS_SEPARATOR : \T_NAME_QUALIFIED:
                        if ($expected) {
                            $name .= $token[1];
                        }
                        continue 2;
                    case \T_NAMESPACE:
                    case \T_CLASS:
                    case \T_INTERFACE:
                    case \T_TRAIT:
                        $expected = $token[0];
                        $name = '';
                        continue 2;
                    case \T_CURLY_OPEN:
                    case \T_DOLLAR_OPEN_CURLY_BRACES:
                        $level++;
                }
            }
            if ($expected) {
                switch ($expected) {
                    case \T_CLASS:
                    case \T_INTERFACE:
                    case \T_TRAIT:
                        if ($name && $level === $minLevel) {
                            $classes[] = $namespace . $name;
                        }
                        break;
                    case \T_NAMESPACE:
                        $namespace = $name ? $name . '\\' : '';
                        $minLevel = $token === '{' ? 1 : 0;
                }
                $expected = null;
            }
            if ($token === '{') {
                $level++;
            } elseif ($token === '}') {
                $level--;
            }
        }
        return $classes;
    }
    /********************* caching ****************d*g**/
    /**
     * Sets auto-refresh mode.
     */
    public function setAutoRefresh(bool $on = \true) : self
    {
        $this->autoRebuild = $on;
        return $this;
    }
    /**
     * Sets path to temporary directory.
     */
    public function setTempDirectory(string $dir) : self
    {
        \Packetery\Nette\Utils\FileSystem::createDir($dir);
        $this->tempDirectory = $dir;
        return $this;
    }
    /**
     * Loads class list from cache.
     */
    private function loadCache() : void
    {
        if ($this->cacheLoaded) {
            return;
        }
        $this->cacheLoaded = \true;
        $file = $this->getCacheFile();
        // Solving atomicity to work everywhere is really pain in the ass.
        // 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19)
        // so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
        // 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock.
        $lock = \defined('PHP_WINDOWS_VERSION_BUILD') ? $this->acquireLock("{$file}.lock", \LOCK_SH) : null;
        $data = @(include $file);
        // @ file may not exist
        if (\is_array($data)) {
            [$this->classes, $this->missingClasses, $this->emptyFiles] = $data;
            return;
        }
        if ($lock) {
            \flock($lock, \LOCK_UN);
            // release shared lock so we can get exclusive
        }
        $lock = $this->acquireLock("{$file}.lock", \LOCK_EX);
        // while waiting for exclusive lock, someone might have already created the cache
        $data = @(include $file);
        // @ file may not exist
        if (\is_array($data)) {
            [$this->classes, $this->missingClasses, $this->emptyFiles] = $data;
            return;
        }
        $this->classes = $this->missingClasses = $this->emptyFiles = [];
        $this->refreshClasses();
        $this->saveCache($lock);
        // On Windows concurrent creation and deletion of a file can cause a error 'permission denied',
        // therefore, we will not delete the lock file. Windows is peace of shit.
    }
    /**
     * Writes class list to cache.
     * @param  resource  $lock
     */
    private function saveCache($lock = null) : void
    {
        // we have to acquire a lock to be able safely rename file
        // on Linux: that another thread does not rename the same named file earlier
        // on Windows: that the file is not read by another thread
        $file = $this->getCacheFile();
        $lock = $lock ?: $this->acquireLock("{$file}.lock", \LOCK_EX);
        $code = "<?php\nreturn " . \var_export([$this->classes, $this->missingClasses, $this->emptyFiles], \true) . ";\n";
        if (\file_put_contents("{$file}.tmp", $code) !== \strlen($code) || !\rename("{$file}.tmp", $file)) {
            @\unlink("{$file}.tmp");
            // @ file may not exist
            throw new \RuntimeException("Unable to create '{$file}'.");
        }
        if (\function_exists('opcache_invalidate')) {
            @\opcache_invalidate($file, \true);
            // @ can be restricted
        }
    }
    /** @return resource */
    private function acquireLock(string $file, int $mode)
    {
        $handle = @\fopen($file, 'w');
        // @ is escalated to exception
        if (!$handle) {
            throw new \RuntimeException("Unable to create file '{$file}'. " . \error_get_last()['message']);
        } elseif (!@\flock($handle, $mode)) {
            // @ is escalated to exception
            throw new \RuntimeException('Unable to acquire ' . ($mode & \LOCK_EX ? 'exclusive' : 'shared') . " lock on file '{$file}'. " . \error_get_last()['message']);
        }
        return $handle;
    }
    private function getCacheFile() : string
    {
        if (!$this->tempDirectory) {
            throw new \LogicException('Set path to temporary directory using setTempDirectory().');
        }
        return $this->tempDirectory . '/' . \md5(\serialize($this->getCacheKey())) . '.php';
    }
    protected function getCacheKey() : array
    {
        return [$this->ignoreDirs, $this->acceptFiles, $this->scanPaths, $this->excludeDirs, 'v2'];
    }
}

```
