ExtensionsLoader – trochu pokročilejší ModelLoader
- nanuqcz
- Člen | 822
Ahoj, pro svůj připravovaný Timmy CMF jsem napsal třídu / službu pro dynamické načítání různých věcí (modelů, služeb, presenterů) – zatím to teda umí jenom ty modely :-)
Co to umí
- pro přidání modelu není třeba editovat config, modely se načtou
automaticky (stačí aby implementovaly rozhraní
Timmy\IModel
) - využívá se dat z RobotLoaderu, takže modely můžou být uložené kde chcete a v jakékoli namespace chcete
- modelu se při vytváření předávají automaticky jen ty služby/parametry, o které v konstruktoru žádá
Prosím buďte shovívaví, psal jsem to dneska a ještě hodně věcí to tam chce dodělat, každopádně načítání modelů je už funkční. Prosím o vaše názory :-)
config.neon
services:
dibi:
class: DibiConnection
arguments: [%database%]
extensionsLoader:
class: Timmy\ExtensionsLoader
arguments: [%appDir%, @container]
bootstrap.php
...
$container = $configurator->loadConfig(__DIR__ . '/config.neon');
$container->extensionsLoader->load(); //load models that implement Timmy\IModel
...
IModel.php
namespace Timmy;
/**
* IModel
* interface for models with autoloading ability
*
* @author Michal Mikoláš <nanuqcz@gmail.com>
* @package Timmy
*/
interface IModel
{
/** name of service */
/**const NAME;*/
}
app/models/ArticlesModel.php
class ArticlesModel implements Timmy\IModel
{
/** name of service */
const NAME = 'articlesModel';
//služba `dibi` se předá automaticky
public function __construct(DibiConnection $dibi) {
$this->dibi = $dibi;
}
public function getArticle($id){
// ...
}
}
Presenter
public function renderDefault(){
$this->getService('articlesModel')->getArticle(...);
}
ExtensionsLoader.php
namespace Timmy;
use \Nette,
\Nette\Caching\Cache,
\Nette\Reflection;
/**
* ExtensionsLoader
* Autoloading and autosetup CMS extensions and models
*
* @author Michal Mikoláš <nanuqcz@gmail.com>
* @package Timmy
*/
class ExtensionsLoader extends Nette\Object
{
/** @var Cache */
protected $cache;
/** @var \Nette\DI\Container */
protected $context;
/** @var string */
protected $baseDir;
public function __construct($baseDir, $context)
{
$this->baseDir = $baseDir;
$this->context = $context;
/** @todo invalidate cache */
$this->cache = new Cache($this->context->cacheStorage, 'Timmy.ExtensionsLoader');
}
/**
* Loads CMS extensions
*/
public function load()
{
$interfaces = array(
'Timmy\\IModel',
'Timmy\\IServiceFactory', /** @todo */
'Timmy\\IRouterSetup', /** @todo */
'Timmy\\IControl', /** @todo */
'Timmy\\ISearchable', /** @todo */
);
// Find classes in appDir folder
$classes = $this->loadClasses($this->baseDir, $interfaces);
// Create models services
foreach($classes['Timmy\\IModel'] as $class){
$className = $class['class'];
$this->context->addService($class['constants']['NAME'], function($container) use ($className){
return $container->extensionsLoader->createService($className);
});
}
}
/**
* Load classes that implements chosen extensions
* @param string
* @param array
*/
protected function loadClasses($baseDir, $interfaces)
{
if (isset($this->cache['classes'])) return $this->cache['classes'];
$classes = array();
foreach($interfaces as $interface){
$classes[$interface] = array();
}
foreach($this->context->robotLoader->getIndexedClasses() as $class => $filename){
if (strpos($filename, $baseDir) !== FALSE) {
$this->filterClass($class, $interfaces, $classes);
}
}
$this->cache['classes'] = $classes;
return $classes;
}
/**
* Filter class by their interfaces and add it to $classes array
* @param string
* @param array
* @param array
*/
protected function filterClass($class, $interfaces, & $classes)
{
$reflection = new Reflection\ClassType($class);
$classInterfaces = array_keys( $reflection->getInterfaces() );
foreach(array_intersect($classInterfaces, $interfaces) as $interface) {
$classes[$interface][$class]['class'] = $reflection->getName();
$classes[$interface][$class]['constants'] = array();
$classes[$interface][$class]['constants']['NAME'] = $class::NAME; /** @todo throw exception if constant not exists */
$classes[$interface][$class]['arguments'] = array();
foreach($reflection->getConstructor()->getParameters() as $parameter){
$classes[$interface][$class]['arguments'][] = $parameter->name;
}
}
}
/**
* Creates service with auto-assign constructor parameter values
* @param string
* @return mixed
*/
public function createService($class)
{
$reflection = new Reflection\ClassType($class);
$args = array();
foreach($reflection->getConstructor()->getParameters() as $param){
$args[$param->name] = $this->getContainerArg($param->name);
}
return $reflection->newInstanceArgs($args);
}
/**
* Find and return container service or param (services first)
* @param string
* @return mixed
*/
public function getContainerArg($arg)
{
if ($this->context->hasService($arg)) {
return $this->context->$arg;
}
return $this->context->params[$arg];
}
}
Editoval xxxObiWan (31. 7. 2011 23:59)
- Tharos
- Člen | 1030
Je to určitě velmi zajímavé, ale neodpustím si jednu jedovatou poznámku. :) Trochu mě k tomu vede i to, že se teď s podobnými „model loadery“ roztrhl pytel…
Jaká je výhoda takovéhoto „model loaderu“ oproti použití DI kontejneru, který je dostupný už přímo v Nette? Nemohu se ubránit dojmu, že jde v podstatě jen o jeho funkčně chudší analogii.
Pokud by Ti například vadilo, že při použití DI kontejneru musíš
dostupné služby ručně specifikovat v konfiguračním souboru (nebo jiným
způsobem), nic Tě neomezuje v tom udělat jeho potomka tak, aby služby
automaticky vytvářel i z indexovaných potomků IModel
.
Nicméně osobně jsem přesvědčen, že je k nezaplacení, když jsou
všechny dostupné služby někde na jednom místě vyjmenované.
Nechci nijak od používání „model loaderu“ odrazovat. Jenom se ale nemohu zbavit pocitu, že je to něco jako kdyby někdo napsal nějakou funkčně chudší variantu Nette formulářů… Jaké jsou ty výhody oproti použití DI+Kontejneru?
Editoval Tharos (1. 8. 2011 0:12)
- nanuqcz
- Člen | 822
Díky za názor :-)
nic Tě neomezuje v tom udělat potomka DI Containeru tak, aby služby automaticky vytvářel i z indexovaných potomků IModel
O tomhle způsobu jsem zatím nikde neslyšel/nečetl, nemám tušení jak by takové řešení vypadalo
Jaké jsou ty výhody oproti použití DI+Kontejneru?
Chci vytvořit systém, který bude rozšiřitelný o nové presentery a moduly jednoduše – stylem „Nahrej nové soubory na FTP a jeď“, nikoli stylem „Nahrej nové soubory na FTP, pak edituj config, pak bootstrap, a pak jeď“ :-) A tohle je zatím jediný uživatelsky přívětivý způsob, který mě napadl (on ten ExtensionsLoader nebude načítat jen modely, ale i komponenty, nebo celé moduly).
(o tom způsobu podědit DI Container zkusím něco pohledat – nebo pokud máš zajímavý odkaz, pošli, díky)
Editoval xxxObiWan (1. 8. 2011 0:25)
- Tharos
- Člen | 1030
Je více možností, jak to, co popisuješ, vyřešit za pomocí vestavěných DI nástrojů.
Ještě elegantnější, než úprava DI kontejneru (který by IMHO správně
žádné hledání v indexovaných třídách provádět neměl, to není jeho
starost), by mohla být vlastní varianta konfigurátoru
(Nette\Configurator
). V něm se konfiguruje právě „globální
kontejner“, a to tak, že se nejprve zaregistrují služby vytvářené
továrními metodami v konfigurátoru samotném obsažené a poté se
v metodě loadConfig
registrují (mimo jiné) další služby na
základě konfiguračního souboru.
Není přece problém vytvořit si vlastní konfigurátor, který bude dědit z toho výchozí v Nette, a pak třeba v konstruktoru anebo ve vlastní vyhrazené metodě registrovat do „globálního konfigutátoru“ podle nějakých pravidel služby, které udržuje v indexu RobotLoader.
Těmi pravidly by mohlo být klidně i to, že konfigurátor projde nějakou složku „plugins“ na serveru. Ty si můžeš zavést konvenci, že například každý plugin musí obsahovat v rootu své složky nějaký soubor s meta-informacemi. Konfigurátor se z toho souboru dozví, jaké služby má pro daný modul zaregistrovat a také tam může mít informaci, jakým způsobem se ty služby mají instaciovat, jaké se jim mají předat další služby a podobně (tak bys třeba mohl řešit vzájemné závislosti modulů?). Požadavek „nahraj na server a jeď“ je tímto splněn.
Nebylo by to hezčí a „více Nette“ řešení?
Editoval Tharos (1. 8. 2011 0:49)
- nanuqcz
- Člen | 822
Nebylo by to hezčí a „více Nette“ řešení?
Popravdě, po přečtení tvojeho příspěvku se mi líbí obě řešení, jak tvoje, tak moje původní :-) Teď už chápu, že podědit Nette configurator je to pravé „čisté“ řešení. Každopádně ExtensionsLoader bude v Timmym existovat tak jako tak (bude provádět věci, které Configuratoru určitě nepřísluší), takže ještě popřemýšlím, jestli nechám i modely na něm, nebo ne.
Editoval xxxObiWan (1. 8. 2011 1:09)
- Patrik Votoček
- Člen | 2221
- Používání jednoho velkého kontaineru se mě moc nelíbí (je dost možné že změním názor).
- V plnohodnotném používání více kontainerů (subkontainerů) mě
brání hlavně to že Configurátor není rozdělen na více tříd.
- Default Services + Params
- Neon Services + Params
- ostatní
Tj. pokud bych chtěl využít plnou sílu subkontainerů musel bych zkopírovat vekou spoustu kódu z Nette\Configurátor nebo to celé napsat znova.
- Patrik Votoček
- Člen | 2221
Ano napsal jsem si proto chudšího příbuzného Nette\DI\Container-u. Nicméně tohle byl jenom jeden z důvůdů proč jsem to tak udělal.
- Tharos
- Člen | 1030
Patrik Votoček napsal(a):
- Používání jednoho velkého kontaineru se mě moc nelíbí (je dost možné že změním názor).
Úplně mám pocit, že Tvůj postoj ovlivňuje právě ta trochu
„nestandardní“ implementace DI v Nette (v uvozovkách proto, protože co
je zde standardní?). Tím mám na mysli zejména předávání kontejneru, což
dost zavání service locatorem. Pokud bys skutečně předával závislosti
striktně instanci po instanci a přes konstruktor (třebaže za použití
nějakého autowiringu), velký kontejner by Tě pak myslím ani moc netrápil.
Ale co naplat, když každá druhý má v Nette tendenci napsat si
„ModelLoader“, abstraktní BaseModel s metodou setContext
a
jeho potomkům automaticky předá globální kontejner i se službami, jako je
Application
, že. :)
Výhoda jednoho kontejneru mi přijde v tom, že můžeš mít všechny závislosti aplikace hezky pohromadě na jednom místě. A při „čistém“ DI o tom kontejneru skutečně skoro nikdo neví, a tak nehrozí, že by si nějaká třída sahala pro služby, po kterých jí nic není.
- V plnohodnotném používání více kontainerů (subkontainerů) mě brání hlavně to že Configurátor není rozdělen na více tříd.
Máš naprostou pravdu, že pokud si chceš nahrávat vlastní služby
z nějakého vlastního services.neon
, v podstatě musíš
udělat copy + paste minimálně od řádku 114 do 139 z originálního
konfigurátoru (v praxi ale ještě o něco více, je tam právě i ta
privátní metoda generateCode
…). Minimálně oddělení toho
parsování služeb z konfiguračního souboru by určitě nebylo naškodu.
Editoval Tharos (1. 8. 2011 9:06)
- nanuqcz
- Člen | 822
Máš naprostou pravdu, že pokud si chceš nahrávat vlastní služby z nějakého vlastního
services.neon
, v podstatě musíš udělat copy + paste minimálně od řádku 114 do 139 z originálního konfigurátoru (v praxi ale ještě o něco více, je tam právě i ta privátní metodagenerateCode
…)
Právě jste mě přesvědčili, že ExtensionsLoader je pro mě ta správná volba :-) A už chápu, proč se s různými ModelLoadery jak říkáš roztrhl pytel – i když já jsem si žádných nevšiml, jen toho co je v kuchařce. Proto jsem tady taky dal ten svůj ;-)