DI – Modely a jejich vztahy – best practice

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

Současně stavím jednu složitější aplikaci a cítím, že neznám správné řešení.

Aplikace má modely,které jsou načítány pomocí modelLoaderu (inspiroval jsem z návodu Michaela Moravce, za což mu patří můj dík):

<?php

namespace Model;

use Nette;


final class ModelLoader extends Nette\Object
{


	private $container;

	private $modelContainer;

	private $models = array();



	public function __construct(Nette\DI\Container $container)
	{
		$this->container = $container;
		$modelContainer = new Nette\DI\Container;
		$modelContainer->addService('database', $container->database);
		$modelContainer->addService('cacheStorage', $container->cacheStorage);
		$modelContainer->params = isset($container->params['models']) ? $container->params['models'] : array();
		$modelContainer->freeze();
		$this->modelContainer = $modelContainer;
	}


	public function &__get($name)
	{
		return $this->getModel($name);
	}


	public function createModel($name, $need = TRUE)
	{
		$class = 'Model\\' . ucfirst($name);
		if (!class_exists($class, TRUE)) {
			throw new Nette\InvalidArgumentException("Model '$class' not found");
		}
 		$model = new $class;
 		$model->setContext($this->container); // sem by asi mel jít $this->modelContainer, ale to zase modely nebudou mít přístup mezi sebe
 		return $model;
	}


	public function getModel($name, $need = TRUE)
	{
		$lname = strtolower($name);
		if (!isset($this->models[$lname])) {
 			$this->models[$lname] = $this->createModel($name, $need);
		}
		return $this->models[$lname];
	}

}
?>

V presenteru potřebuji s modely nějakým způsobem pracovat a inicializovat je – např. modelu language nastavím aktivní jazyk, proto jsem si vytvořil v base presenteru jejich továrničky a inicializuji je ve startupu, kde se přidají do systémového kontejneru (nebo by bylo lepší vytvořit vnořený kontejner?)

<?php

namespace Base;

use Nette;


abstract class FrontPresenter extends Nette\Application\UI\Presenter
{

	const MODEL_PRODUCTS = 'products\\products';
	const MODEL_INSTANCES = 'instances\\instances';
	const MODEL_CURRENCIES = 'currencies\\currencies';
	const MODEL_LANGUAGES = 'languages\\languages';


	/** @var persistent */
	public $lang;


	public function startup(){
		parent::startup();
		$this->context->addService('language', $this->detectLanguage());
		$this->context->addService('instance', $this->createInstance());
		$this->context->addService('currency', $this->createCurrency());
	}


	public function getModel($model, $need = TRUE)
	{
		return $this->context->modelLoader->getModel($model, $need);
	}


	public function createCurrency()
	{
		$currencyId = $this->context->instance->currency_id;
		if($currencyId){
			$currency = $this->getModel(self::MODEL_CURRENCIES)->find($currencyId)->fetch(); // osetrit hodnotu pred fetch()
		}else{
			throw new Nette\InvalidStateException();
		}
      		return $currency;
	}


	public function getCurrency()
	{
      		return $this->context->currency;
	}
}
?>

Problém je, že si nejsem jistý, jestli modely patří takto do kontejneru, navíc do systémového kontejneru a jestli vlastně toto řešení je v souladu s myšlenkou DI.
Na druhou stranu modely musí mít možnost přistupovat mezi sebe..

Budu vděčný za jakékoliv nakopnutí od někoho, kdo je vyšším levelu.

Editoval semtex.989 (1. 12. 2011 10:05)

Filip Procházka
Moderator | 4668
+
0
-

Musíš si vybrat, jestli chceš jednoduchý modelLoader, nebo plnohodnotný DIC.

S plnohodnotným DIC budeš psát všechny služby do configu, ano opravdu, do texťáku :) Napíšeš, co se jim má předat, jaká třída je ta služba, … Nette DIC z dev by teď měl umět moc pěkně automaticky doplňovat závislosti do konstruktoru, ale ještě jsem to neměl odvahu testovat :)

Dřív jsem se snažil dělat na všechno statické Containery, abych mohl definice služeb psát v PHP (tam teď směřuješ ty). Došlo mi ale, že tudy cesta nevede. Jakékoliv moje pokusy o hierarchii a vnořené containery teď zpětně vnímám jako nepohodlnou, slepou uličku. Jediná správná cesta™ je všechny služby zapisovat na jednu hromadu (byť do více konfigů, kvůli přehlednosti). Moc se mi to nelíbilo, ale z odstupem několika týdnů mi to už dává smysl.

Vyber si :) Jednoduchost, nebo složitější flexibilita?


TL;DR: V tvém konkrétním případě by ti mohlo pomoct předávat těm modelům v contextu i ten samotný ModelLocator. Díky tomu z jednoho modelu budeš moct volat jiný a nebudeš je muset předávat jeden druhému (context zde bude fungovat jako most).

Nevýhoda: Díky tomu vytvoříš hromadu skrytých zavilostí a časem se ztratíš v tom, který model co používá

semtex.989
Člen | 75
+
0
-

Všechno přeci nejde zapisovat v config texťáku. Třebas takový jazyk, ten se inicializuje takto (z čehož je jasné, že to musí dělat presenter a předat výsledek DI Containeru):

<?php
namespace Base;

class FrontPresenter{

	/** @var persistent */
	public $lang;

        public function startup(){
                parent::startup();
                $this->context->addService('language', $this->detectLanguage());
        }

	protected function detectLanguage(){
	      	$languagesModel = $this->getModel(self::MODEL_LANGUAGES);
      		$languages = $languagesModel ->findAll()->fetchPairs("code", "id");
      		// language from URI
      		if($this->lang && isset($languages[$this->lang])){
			$language = $languagesModel->find($languages[$this->lang])->fetch();
      		// detect language from HttpRequest
		}elseif($detected = $this->getHttpRequest()->detectLanguage(array_keys($languages))){
			$language = $languagesModel->find($languages[$detected])->fetch();
      		// cannot detect, use first in array
      		}else{
			$language = $languagesModel->fetch();
      		}
      		return $language;
	}

}
?>

EDIT: ale s tou nevýhodou máš pravdu, ty skryté závislosti, to je hodně nepříjemná skutečnost…

Mimochodem, není někde nějaký example, co umí DIC co je na GITu?

Editoval semtex.989 (1. 12. 2011 15:00)

22
Člen | 1478
+
0
-

Tento myšlenkový pochod mi není jasný.. detectLang() je jasný kandidát modelu a ve startup by mělo být akorát $this->lang = $this->context->model->detectLang();

edit: nehledě na to, že default hodnotu můžeš nastavit přímo a je to persistent, takže se automaticky z URL získá…

Editoval 22 (1. 12. 2011 15:12)

pawouk
Člen | 172
+
0
-

@22 osobně si nemyslím že detectLang() je jasný kandidát modelu. Ba naopak. Jazyk patří hlavně do prezentační vrstvy, samozřejmě občas se musí zaptat modelu na data v určitém jazyce, ale jeho místo je podle mého názoru v Presenteru.

@semtex.989
No já bych se osobně tomu model loaderu právě raději vyhnul, je celkem v rozporu se samotným DI, hlavní myšlenka DI je: předávej závislosti (setrem nebo v konstruktoru to už je jedno) ale předávej pouze ty závislosti, které jsou potřeba!. Tedy v ideálním případě se podíváš do konstruktoru a ihned vidíš na jakých třídách je závislí, a to je super, nepříjemná okolnos tohot je to, že konstruktor prostě narůstá, nicméně to nic nemění na tom, že to tak je správně. Předat v konstuktoru jeden objekt, který má přistup ke všem objektů není idealní, z toho rozhodně nepoznáte na čem je daný objekt závislí. On už samotný context není ideální, vhodnější by bylo všchny potřebné třídy předávát v konstruktoru presenteru, jenže to je komplikované, tak se to řeší contextem. Nicméně tím že vytvoříte modelLoader který umí „všechno“ a předáváte ho každému objektu rozhodně nejdete cestou správného DI. To už si to můžete rovnou předávat staticky, to je jen o trochu horší. To jen na doplnění, jinak souhlasím s tím že by se vše mělo psát do config.neon.

Elijen
Člen | 171
+
0
-

HosipLan napsal:

Dřív jsem se snažil dělat na všechno statické Containery, abych mohl definice služeb psát v PHP (tam teď směřuješ ty). Došlo mi ale, že tudy cesta nevede.

Mohl bys stručně shrnout, jaké má výhody služby definovat pouze v configu? Zpočátku jsem to zkoušel také, ale narazil jsem na několik omezení, kvůli kterým jsem stejně musel některé továrničky hodit do vlastního Conteineru. Jediná výhoda, která mě napadá, je trochu méně psaní. Zato mě napadá spousty nevýhod.

Editoval Elijen (1. 12. 2011 18:25)

semtex.989
Člen | 75
+
0
-

Domnívám se, že detekci jazyku by si měl zajistit presenter. Pokud by měl model přistupovat k httpRequestu, bral bych to jako porušení logiky (model spoléhající na persistentní proměnnou presenteru), navíc potomci base presenteru mohou tuto metodu přepsat dle potřeby.

No zkusím to pomocí samotného DI auto-wiringu.

1.) Předpokládám tedy, že si mám vytvořit DI container s továrničkami. Ty modely, které továrničku nepotřebují se budou vytvářet přímo instanciováním třídy.

2.) Presenter bude k modelům přistupovat takto:

<?php
$languagesModel = $this->context->modelsContainer->language;
?>

Otázkou mi je, jak config.neon přimět k tomu, aby mi pro služby modelu vytvořil samostatný kontejner
(nacpat služby modelů do systémového kontejneru je asi blbost)

Editoval semtex.989 (1. 12. 2011 18:38)

22
Člen | 1478
+
0
-

Imho Presenter má presentovat, tzn. poslat parametry do modelu a data z modelu presentovat pomocí view. Jestliže žádám data v češtině, předam modelu identifikator jazyka, pokud neni nic takového, ať model dodá default a já ty data zobrazím v šabloně, která odpovídá jazyku nebo default.

Nevidím důvod, proč by se model nemohl postarat o detekci jazyka, pokud nedostane identifikator.

Editoval 22 (1. 12. 2011 22:07)