Autor Zpráva
Tori
Profil
Inspirovaná popisem RobotLoaderu z Nette (kód jsem neviděla) jsem si napsala třídu s podobnou funkčností. Co by se na ní dalo zlepšit, změnit, případně je chybně řešené? Bylo by výhodnější ji napsat jako statickou? Díky moc za rady.

class LoaderException extends Exception { }


class Loader {
	
	/** @var string Cesta k nejvyššímu adresáři aplikace */
	private $appRoot;
	
	/** @var string Cesta ke konfiguráku, z nějž se čtou data.*/
	private $cfgFilePath;
	
	/** @var array Seznam všech souborů aplikace. */
	private $fileList;
	
	/** @var array Nalezené kombinace třída=>soubor */
	private $cfg;
	
	/** @var bool Jsme na produkčním serveru? (rezerva, pokud by to bylo potřeba rozlišovat) */
	private $isProduction;
	
	
	/**
	 * Uloží nastavení 
	 * @param string $appRoot Nejvyšší adresář aplikace. Koncové lomítko se doplní automaticky.
	 * @param string $cfgFile Cesta k souboru, kam se ukládá nastavení autoloaderu. Může být relativní cesta vůči APPROOT.
	 * @param bool $isProduction Na jakém serveru jsme (výchozí je true=produkční).
	 */
	public function __construct($appRoot, $cfgFile, $isProduction=true)	{
		
		if (empty($appRoot))
			throw new LoaderException('Autoloader: Nejvyšší adresář aplikace nebyl nastaven.');
		if (!is_readable($appRoot))
			throw new LoaderException('Autoloader: Nejvyšší adresář aplikace byl chybně nastaven.');
		else
			$this->appRoot = $appRoot . (substr($appRoot, -1) != DIRECTORY_SEPARATOR ? DIRECTORY_SEPARATOR : '');
			
		$this->cfgFilePath = $this->setCfgFile($cfgFile);
		$this->isProduction = isset($isProduction) ? $isProduction : true;
	}
	
	
	/**
	 * Vrací název souboru s definicí třídy.
	 * @param string $class
	 * @return string Cesta k souboru 
	 * @throws LoaderException
	 */
	public function get($class)	{
		$this->getConfig();
		if (isset($this->cfg[$class]))
			return $this->cfg[$class];
		/* Pokud nenalezena třída, updatuj konfigurák */ 
		$this->create();
		if (isset($this->cfg[$class]))
			return $this->cfg[$class];
		throw new LoaderException("Autoloader: Definice třídy $class nebyla nalezena.");
	}
	
	
	/**
	 * Ověří existenci a oprávnění souboru s nastaveními. Pokud nezadán, zkusí použít APPROOT/loader.ini.php.
	 * Automaticky doplňuje absolutní cestu. 
	 * @param string $file
	 */
	private function setCfgFile($file='')	{
		if (empty($file))
			$file = $this->appRoot.'loader.ini.php';
		$file = str_replace($this->appRoot, '', $file);
		return $this->checkCfgFile($this->appRoot.$file);
	}
	
	
	/**
	 * Ověří existenci a oprávnění zadaného souboru.
	 * @return string  
	 * @throws LoaderException
	 */
	private function checkCfgFile($file)	{
		$dir = dirname($file);
		if (!is_writable($file))	{
			if (!is_writable($dir))
				throw new LoaderException('Soubor nastavení neexistuje a nadřazený adresář není zapisovatelný, takže ho nelze vytvořit.');
			if (file_exists($file))
				throw new LoaderException('Soubor nastavení není zapisovatelný.');
		}
		return $file;
	}
	
	
	/**
	 * Projde aplikaci a vytvoří konfigurační soubor.
	 */
	private function create()	{
		$this->scan();
		$this->parse();
		$this->saveConfig();
	}
	
	
	/**
	 * Načte nastavení ze souboru (jen jednou). 
	 * @throws LoaderException
	 */
	private function getConfig()	{
		if (!empty($this->cfg))
			return null;
		if (!is_readable($this->cfgFilePath))
			throw new LoaderException('Autoloader: Zadaný soubor s nastaveními neexistuje.');

		$this->cfg = parse_ini_file($this->cfgFilePath, false);
	}
	
	
	/**
	 * Projde rekurzivně všechny adresáře aplikace a vyhledá .php soubory. 
	 */
	private function scan($dir='') {
		$dir = empty($dir) ? $this->appRoot : $dir;
		
		foreach(scandir($dir) as $file)	{
			if (is_dir($dir.$file) && $file != '.' && $file != '..')
				$this->scan($dir.$file.DIRECTORY_SEPARATOR);
			if (substr($file, -4) == '.php' && strpos($file, '.ini') === false)
				$this->fileList[] = $dir.$file;
		}
	}
	
	
	/**
	 * Uloží nastavení autoloaderu do souboru.
	 * @throws LoaderException
	 */
	private function saveConfig()	{
		$begin = '; <'."?php /*\n; Nastavení autoloaderu. Tento soubor neupravovat, generuje se automaticky!\n";
		$end = "\n; */\n";
		$errors = '';
		ob_start();
		if (($fh = fopen($this->cfgFilePath, 'wb')) !== false)	{
			fwrite($fh, $begin);
			foreach ($this->cfg as $class=>$file)
				fwrite($fh, "\n$class = \"$file\"");
			fwrite($fh, $end);
			@fclose($fh);
		}
		$errors = ob_get_contents();
		ob_end_clean();
		if ($errors)
			throw new LoaderException("Autoloader: Nebylo možné zapsat soubor nastavení. Chyby: \n$errors");
	}
	
	
	/**
	 * Postupně projde jednotlivé .php soubory a vyhledá definice tříd a rozhraní.
	 * Ukládá relativní cesty vůči AppRoot (bez počátečního lomítka).
	 */
	private function parse()	{
		foreach ($this->fileList as $file)	{
			$src = file_get_contents($file);
			if ($src === false)
				throw new LoaderException("Nelze aktualizovat nastavení autoloaderu, soubor $file je nečitelný. Zkontrolujte oprávnění.");
			$tokens = token_get_all($src);
			foreach ((array)$this->getTokens($tokens) as $class)	{
				$this->cfg[$class] = str_replace($this->appRoot, '', $file);
			}
		}
	}
	
	
	/**
	 * Najde všechny názvy tříd (rozhraní) v tokenizovaném souboru. 
	 * @param array $tokens
	 * @return array
	 */
	private function getTokens($tokens)	{
		$names = array();
		$found = false;
		foreach($tokens as $tok)	{
			if (!is_array($tok))
				continue;
			if ($found && $tok[0] == T_STRING)  {
				$found = false;
				$names[] = $tok[1];
			}
			elseif ($tok[0] == T_CLASS || $tok[0] == T_INTERFACE)
				$found = true;
		}
		return $names;
	}
	
}

// konstanta DS == DIRECTORY_SEPARATOR. 
$cfgFile = APPROOT.'lib'.DS.'Config'.DS.'loader.ini.php';
$loader = new Loader(APPROOT, $cfgFile);

function __autoload($class)	{
	global $loader;
	$file = $loader->get($class);
	include_once(APPROOT . $file);
}
Alphard
Profil
Promiň, jestli to přehlížím, ale blíže konfigurovat, co se má načítat, nejde? Nenašel jsem tam žádnou časovou funkci, kontrolu aktuálnosti řeší jiný script? Také jsem neviděl řešení paralelního přístupu, co když začne velkou aplikaci zpracovávat více scriptů najednou?

Co tě motivuje psat to staticky? Minimálně u parsovacího robota pro to nevidím důvod.

Zmíněný RobotLoader je tady, třeba někomu ušetřím hledání.
Tori
Profil
Alphard:
blíže konfigurovat, co se má načítat, nejde?
Nerozumím otázce. Ta třída je jako pomůcka pro funkci __autoload: z konfiguráku (.ini soubor) načte názvy všech tříd v aplikaci + soubor, ve kterém jsou definované. Pokud konfigurák chybí nebo je zastaralý, vygeneruje nový.

kontrolu aktuálnosti řeší jiný script?
To jsem udělala tak, že pokud se nenajde soubor, ve kterém je definovaná volaná třída, nechá se přegenerovat konfigurák a znova se načte. Pokud ani potom není nalezena (= pokus o vytvoření instance třídy, jejíž definice chybí, který by stejně skončil jako Fatal error), vyhodí se výjimka. Jelikož ji nikde neodchytávám, tak vlastně spadne celá aplikace - ale to by spadla tak či tak. Blbé řešení, ale nenapadlo mne zatím nic lepšího - překlepy a chybějící soubory by se stejně na ostrém serveru neměly objevit.

Také jsem neviděl řešení paralelního přístupu, co když začne velkou aplikaci zpracovávat více scriptů najednou?
Nejvíc času zabere procházení souborů a zjištění, která třída je kde definovaná, ale pokud zrovna nepřejmenuju třídu na ostrém serveru ve špičce, tak k tomu nedojde. Takže zbývá paralelní čtení konfiguráku pomocí parse_ini_file - nevím, jak funguje interně, jestli by bylo lepší načíst soubor pomocí file_get_contents + parse_ini_string?

Co tě motivuje psat to staticky?
Jen jsem se ptala na názor - nevěděla jsem, jak jinak dostat instanci do funkce __autoload kromě 1.instancovat mimo funkci + global, nebo 2.statická třída. To global se mi tam nějak nelíbí, ale jestli to není proti dobrým programátorským mravům, tak čert ho ber. :)
Alphard
Profil
Tori:
Nejvíc času zabere procházení souborů a zjištění, která třída je kde definovaná, ale pokud zrovna nepřejmenuju třídu na ostrém serveru ve špičce, tak k tomu nedojde.
Určitě. Teď to asi nebude vadit, jen jsem si vzpomněl, že u Nette se to řešilo a po použití nějaké zamykacího souboru (jeden pracuje, zbytek čeká) se výrazně zlepšil čas. (Pozor, při rozpoznávání controllerů z url to nemusíš přejmenovat ty, viz dále).

Nerozumím otázce. Ta třída je jako pomůcka pro funkci __autoload: z konfiguráku (.ini soubor) načte názvy všech tříd v aplikaci + soubor, ve kterém jsou definované.
Opět jsem si vzpomněl na Nette, nedávno byl na fóru problém s nevhodně nakonfigurovaným robotem, mj. parsoval i Zend, a čas byl údajně až 4 minuty. Tj. hodí se možnost říct robotovi, tento adresář ignoruj.

To jsem udělala tak, že pokud se nenajde soubor, ve kterém je definovaná volaná třída, nechá se přegenerovat konfigurák a znova se načte.
Aha, nevšiml jsem, dobrý nápad. Jako nevýhoda mě napadá, že při každé chybné url (tj. v MVC požadavek na neexistující controller) se bude generovat nový soubor.

Jen jsem se ptala na názor
Viz http://cz.php.net/manual/en/function.spl-autoload-register.php#102180


Jinak mě neber do důsledku, moc vrtám, čekal jsem, jestli odpoví někdo jiný, ale nakonec jsem napsal pár poznámek. Předhazuji různé nápady, prostě je filtruj.
Tori
Profil
Alphard:
při každé chybné url (tj. v MVC požadavek na neexistující controller) se bude generovat nový soubor.
Aha, tahle souvislost mi nedošla, díky. Popřemýšlím (a taky kouknu na fórum Nette, co se tam řešilo s RobotLoaderem). Možnost ignorovat zadané adresáře se určitě hodí. Zvažovala jsem to u dibi-minified, která se zpracovávala jednoznačně nejdéle (z toho co používám), ale ještě uvidím, jak rychle to půjde na wedosu. Na localhostu se zpracuje dibi za 0.13s (rozebrání na tokeny+zpracování pole), což se mi zatím zdá snesitelné.

Jinak SPL na mne teprve čeká, zatím z něj nic neumím využít. Za nápady každopádně děkuji :-)
Tori
Profil
Alphard:
při každé chybné url (tj. v MVC požadavek na neexistující controller) se bude generovat nový soubor
Až teď jsem se k tomu zase dostala - a kupodivu tohle zrovna funguje dobře, protože router nepoužívá method_exists, ale poskládá si název souboru podle "<nazev-z-url>Controller.php" a hledá soubor v předdefinované složce. Sice to narušuje nezávislost jmen souborů na jménech tříd, ale snad je to ještě přijatelná cena (u malého webu).

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: