ExtensionsLoader – trochu pokročilejší ModelLoader

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
nanuqcz
Člen | 822
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-
  1. Používání jednoho velkého kontaineru se mě moc nelíbí (je dost možné že změním názor).
  2. 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.
    1. Default Services + Params
    2. Neon Services + Params
    3. 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.

nanuqcz
Člen | 822
+
0
-

@Patrik Votoček: Chápu to dobře, že jsi tím pádem spíš pro použití ModelLoader-like řešení? :-)

Patrik Votoček
Člen | 2221
+
0
-

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
+
0
-

Patrik Votoček napsal(a):

  1. 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í.

  1. 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
+
0
-

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…)

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 ;-)