Krasný index.php s vylepšeným DI Containerem

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip Procházka
Moderator | 4668
+
0
-

Štval mě „zbytečný“ soubor app/boostrap.php, takže jsem ho z projektu úplně vyhodil a můj www/index.php nyní vypadá takto

<?php

// load bootstrap file
require_once __DIR__ . '/../app/autoload.php';

// Run the application
$app = new Kdyby\Application\Application();
$app->run();

Cože?!! Jak se mu to povedlo? To chci taky!!

Nejprve upravit Application

Zjednodušená verze:

class Application extends Nette\Application\Application
{
	public function __construct()
	{
		$configurator = new Kdyby\Config\Configurator();

		// inject application instance
		$container = $configurator->getContainer();
		$container->configureService('application', $this);

		// dependencies
		parent::__construct(
			$container->nette->presenterFactory,
			$container->router,
			$container->httpRequest,
			$container->httpResponse,
			$container->session
		);
	}

}

Plná verze

Tedy to, co Nette sandbox dělá v app/boostrap.php já mám v konstruktoru Application

Autoloading

Celý app/boostrap.php jsem osekal pouze na načítání tříd a přejmenoval ho na app/autoload.php. Můžu ho tedy využít i v testech i v aplikaci a navzájem si to nic nenastavuje, jenom nastaví PSR-0 autoloadery a možná (magicky) RobotLoader.

Container::configureService()

Nojo, ale když vytvoříš instanci mimo container, tak ji nemůžeš Containerem nastavit!

Vskutku nepříjemná situace, ale Kdyby ji řeší velice jednoduše. Do vlastního rozšíření pro kontejner si implementujeme následující metodu

	/**
	 * @param \Nette\Utils\PhpGenerator\ClassType $class
	 */
	public function afterCompile(Code\ClassType $class)
	{
		$container = $this->getContainerBuilder();

		/** @var \Nette\DI\ServiceDefinition $def */
		foreach ($container->getDefinitions() as $name => $def) {
			if ($def->class == 'Nette\DI\NestedAccessor' || $def->class === 'Nette\Callback' || $name === 'container' || !$def->shared) {
				continue;
			}

			$createBody = $class->methods[Nette\DI\Container::getMethodName($name)]->body;
			if ($lines = Nette\Utils\Strings::split($createBody, '~;[\n\r]*~mi')) {
				array_shift($lines); // naive: first line is creation

				$configure = $class->addMethod('configure' . ucfirst(strtr($name, '.', '_')));
				$configure->visibility = 'private';
				$configure->addParameter('service')->typeHint = $def->class;
				$configure->setBody(implode(";\n", $lines));
			}
		}

		$configure = $class->addMethod('configureService');
		$configure->addParameter('name');
		$configure->addParameter('service');
		$configure->setBody(
			'$this->{"configure" . ucfirst(strtr($name, ".", "_"))}($service);' . "\n" .
			'if ($this->hasService($name)) {' . "\n" .
			'	$this->removeService($name);' . "\n" .
			'}' . "\n" .
			'$this->addService($name, $service);'
		);
	}

Voala! Jakoukoliv službu si nyní můžu vytvořit sám, zavolat nad ní $container->configureService() a on mi ji nastaví podle toho, co mám v configu (kromě parametrů konstruktoru, logicky) a vloží do containeru.

Když nepočítám čas na psaní příspěvku, tak jsem ji napsal před 10ti minutami.

A úplně nejlepší by bylo, kdyby Davídek tohle přijal jako výzvu a implementovat to přímo do Nette a mnohem elegantněji :)

knyttl
Člen | 196
+
0
-

Like.

David Grudl
Nette Core | 8147
+
0
-

Třeba takto by mělo jít volat $configuredAbc = $container->createConfigureAbc($abc);

	factories:
		configureAbc:
			class: Authenticator
			parameters: [service]
			factory: ?(%service%)
Filip Procházka
Moderator | 4668
+
0
-

@**David Grudl**: Přemýšlel jsem nad tím, že si to budu nějak generovat pomocí services, ale pak mě od toho odradilo, že bych si „zaneřádil“ DIC (a hlavně panel).

Editoval HosipLan (9. 2. 2012 23:41)

Aurielle
Člen | 1281
+
0
-

Mnohem pěknější řešení než moje naprasené, které ale dalo podnět k řešení problému. Což nic nemění na tom, že se mi tenhle způsob bootstrapování moc a moc líbí :)

hrach
Člen | 1834
+
0
-

Do praxe imo moc nepouzitelne. Osobne mam 3 bootstrapy.

  • bootstrap.php includujici nette, nastavujici debugger, atp.
  • bootstrap_web.php – includuje bootstrap.php, pridava routing a spousti aplikaci
  • bootstrap_cli.php – includuje bootstrap.php, pridava routing cli verze, detekuje server, nastavuje jinak debugovani, spousti aplikaci.

Bootstrap.php pak používám pro jednorázové skripty – k nezaplacení. S tím tvým indexem se pokaždé dostanu do aplikace … možná to takhle bude fungovat pro blog, cms …

Filip Procházka
Moderator | 4668
+
0
-

Do aplikace mám tři vstupy:

  • www/index.php – viz výše, prostě vstup pro uživatele (přes prohlížeč)
  • $ app/console – php script, který místo Kdyby\Application\Application spouští úplně stejně Kdyby\Console\Application
  • $ phpunit – přes phpunit.xml se načte soubor tests/boostrap.php, který nezapne aplikaci, ale jen vytvoří configurator a zpřístupní container staticky.

Všechny tři využívají app/autoload.php a navzájem si nepřekáží. Kde je problém?

Můj boostrap_cli by tedy byl pouze vyčleněním 4 (slovy čtyř) řádků do samostatného souboru :)

Mně to tedy připadá velice použitelné.

Editoval HosipLan (10. 2. 2012 13:11)

hrach
Člen | 1834
+
0
-

@HosipLan: no ano, v tom případě to máš přesně jako já, jenom sis místo mých tří bootstrapů na jednom místě udělal 3 na různých místech :D Každopádně congrats :D Pravda, na testy sem zapomněl, ale ty řeším mimo app adresář…

ptacek.pavel
Člen | 27
+
0
-

Osobně jsem ze stejného důvodu vyhodil z bootstrap $application->run() a volám ho v index.php . Moje pointa je taková, že pokud vyrábím „servisní skript“ který (z jakéhokoliv důvodu) nechci zabalovat do presenteru (ConsoleModule\NecoPresenter) tak si hodím include na bootstrap a mám nastaveno vše, co potřebuju.

ad $ php index.php module:presenter:action resp. tvoje app/console – imho zbytečné rozdělovat, když stačí přidat CliRouter() ne?

Jinak problém tří bootstrapů řeším momentálně takto:

<?php
// Load default config + configs per developer machine --> with graceful degradation
$configurator->addConfig(__DIR__ . '/config/config.neon');
if($_SERVER['SERVER_NAME'] == 'someproject.com') { // live hosting
    $configurator->addConfig(__DIR__ . '/config/live.neon');
    \Nette\Diagnostics\Debugger::enable('85.13.110.131');
}
elseif($_SERVER['SERVER_NAME'] == 'someproject.animalgroup.cz') { // stage hosting
    $configurator->addConfig(__DIR__ . '/config/stage.neon');
    \Nette\Diagnostics\Debugger::enable(\Nette\Diagnostics\Debugger::DEVELOPMENT);
}
elseif($_SERVER['SERVER_NAME'] == 'someproject.ourci.tld') { // pre-staging environment, it's the one with git installation
    $configurator->addConfig(__DIR__ . '/config/ourci.neon');
    \Nette\Diagnostics\Debugger::enable(\Nette\Diagnostics\Debugger::DEVELOPMENT);
}
elseif($_SERVER['HTTP_HOST'] == 'someproject.ag') { // foglcz's machine
    $configurator->addConfig(__DIR__ . '/config/foglcz.neon');
}
elseif($_SERVER['SERVER_ADMIN'] == 'admin@tomas-cap-pc.RD') { // capino's machine
    $configurator->addConfig(__DIR__ . '/config/capino.neon');
}
?>

-- to je kvůli tomu, že je z principu live/stage/dev, kdy stage = náhled pro klienta a dev = všechny mašiny všech vývojářů (neboť to neděláme na devel serveru, že.)

Filip Procházka
Moderator | 4668
+
0
-

Já ale vůbec nepoužívám CliRouter a ani ho používat nechci. Daleko více mi vyhovuje Symfony konzole.

ptacek.pavel
Člen | 27
+
0
-

Hosiplan: Chápu tedy, že moje načítání configů bych přesunul do obou konstruktorů application? Resp. udělal Application Abstract, od kterého by dědila Application a Console\Application? Nechci totiž volat tvoje Kdyby\(Console|Application)\Application (logicky.)

Pak bych měl problém s tím, že abstract neudělám (dependency hell) a tím pádem config loading budu mít ve dvou konstruktorech metodikou ctrl+c a ctrl+v, nebo bych to zabalil do nějaké statické metody (Application::loadConfigs($configurator))

Hrozně bych chtěl mít jeden entry point, který mi vrátí $application, spíše ale $container – což může právě bootstrap.php . Schovávat to do konstruktoru aplikace mi přijde nešikovné..

edit: všechno ostatní, co potřebuju, můžu nastavit v .neon souborech.. Routování, konekce, služby, .. Můj bootstrap by tedy víceméně mohl jen načíst config a vrátit container – v indexu pak:

<?php
$container = require '../app/bootstrap.php';
$container->application->run();
?>

Zatímco v servisním skriptu:

<?php
$container = require '../../app/bootstrap.php';
$dibi = $container->dibi;
// etc.
?>

Editoval ptacek.pavel (12. 2. 2012 14:56)

Filip Procházka
Moderator | 4668
+
0
-

Já mám Nettí Configurator obalený do svého Configuratoru a s tím pracuje moje Application.

Tam bych tedy měl logiku načítání těch tvých configu. Pokud Application nechci, zavolám jen něco jako

require_once __DIR__ . '/../app/autoload.php';
$config = new Kdyby\Config\Configurator;
$container = $config->getContainer();

a píšu script ;) Ať žije kompozice! :P

Editoval HosipLan (12. 2. 2012 14:53)

ptacek.pavel
Člen | 27
+
0
-

HosipLan: jsem ještě doeditoval last post – sorry :-)

… každopádně pointa těch configů je, že na každém projektu dělají jiní lidé – tzn. sice všude je (config|live|stage|ourci).neon, ale na některých projektech je inject pouze capino.neon a někde jenom foglcz.neon někde je obojí etc – nejde/nechci to dělat univerzálně v libs/Kdyby/Configurator.php – potřebuju to někde v app.

Nakonec mi přijde, že je to spíš o názvosloví – ty tomu říkáš autoload, já bootstrap což mi přijde logičtější kór když by to jenom vrátilo container :-)

Filip Procházka
Moderator | 4668
+
0
-

Můj autoload nastavuje načítání tříd. Nic víc. Tedy to není problém v názvosloví :)

Co se týče těch configů… To je otázka implementace, třídy nejsou final od toho, aby se dědily. Tohle moje je obecné a funkční. Pokud potřebuji něco specifického pro projekt, tak si to prostě upravím.

Hledáte problémy, tam kde žádné nejsou ;)

juzna.cz
Člen | 248
+
0
-

Obecne mi index.php a bootstrap.php tak jak jsou v sandboxu/skeletonu prijde takovy nelogicky. Jaky smysl toto rozdeleni vlastne ma?

Duvod: casta je potreba vice vstupnich bodu do aplikace. Mam to presne tak, jak rikaji @hrach a @hosiplan: 1/ web 2/ konzole 3/ testy. Ve vsech 3 potrebuji mit

Problem: bootstrap neni mozne pouzit samostatne, protoze mu chybi konstanty urcujici cesty k aplikaci (WWW_DIR, APP_DIR, LIBS_DIR) a stejne tak neni znovupouzitelny, protoze okamzite spousti aplikaci. Prijde mi tedy jako takovy jediny vstupni bod do aplikace, kteremu ale neco chybi (to ty cesty).

Reseni od @hosiplan se mi zase moc nelibi, protoze vyzaduje moc kodu „nad Nette“. Mam radeji lehci reseni.

Nejlogictejsi mi prijde reseni, ktere tady zminuje @hrach, kde vsak drobitko zlobi nazvoslovi. bootloader znaci, ze natahuje a spousti aplikaci, coz u nej nedela (lepsi nazev me ale momentalne nenapada).

Jednoduche reseni by znamenalo pouze trosku zamichat index a bootstrap:

  • bootstrap by byl samostatny a znovupouzitelny – obsahoval by tedy definice vsech cest, ale primo aplikaci uz by nespoustel. O to by se staral index
  • index by pouze nacetl bootstrap a spustil aplikaci (2 radky);
  • konzole jako druhy vstupni bod by se velice podobala indexu – nacetla by bootstrap a spustila si dvoji aplikaci (at uz Nette s CliRouterem, nebo Symfony; opet 2 radky);
  • testy by si nacetly bootstrap a delaly si svoje testovani;
  • abcxyz neboli jakykoliv dalsi skript/daemon by si nacetl bootstrap a delal by si svoje, treba zustal bezet a komunikoval by si po siti, sledoval servery a tak obecne co by se mu zachtelo.

Rozsirene reseni popsal @hrach, stejne jako predchozi ale vytahuje z bootstrapu cast pro web a pro cli.

nanuqcz
Člen | 822
+
0
-

juzna.cz napsal(a):

  • bootstrap by byl samostatny a znovupouzitelny – obsahoval by tedy definice vsech cest, ale primo aplikaci uz by nespoustel. O to by se staral index
  • index by pouze nacetl bootstrap a spustil aplikaci (2 radky);
  • konzole jako druhy vstupni bod by se velice podobala indexu – nacetla by bootstrap a spustila si dvoji aplikaci (at uz Nette s CliRouterem, nebo Symfony; opet 2 radky);
  • testy by si nacetly bootstrap a delaly si svoje testovani;
  • abcxyz neboli jakykoliv dalsi skript/daemon by si nacetl bootstrap a delal by si svoje, treba zustal bezet a komunikoval by si po siti, sledoval servery a tak obecne co by se mu zachtelo.

+1. Konečně vidím v rozdělení index/bootstrap nějaký smysl :-)

juzna.cz
Člen | 248
+
0
-

Nakonec jsem se priblizil jeste o krok vic k @Hosiplanovi a z bootstrapu jsem vyclenil autoload v takovem tvaru, jako ho ma on. Autoload se stara pouze o to, aby byly vsechny potrebne tridy k dispozici, nic vic nedela.

pekelnik
Člen | 462
+
0
-

Asi takhle:

Poznámka: boostrap.php je přímo v root adresáři.

/bootstrap.php

<?php

// 1. Setup parameters
$p['baseDir'] = $base = __DIR__;
$p['config'] = "$base/configs/config.neon";
$p['appsDir'] = "$base/apps";
$p['libsDir'] = "$base/libs";
$p['tempDir'] = "$base/var/temp";
$p['logDir'] = "$base/var/logs";
$p['wwwDir'] = "$base/www";

// 2. Load framework
require $p['libsDir'] . '/Nette/loader.php';

// 3. Create configurator
$configurator = new Configurator();
$configurator->addParameters($p);
$configurator->setTempDirectory($p['tempDir']);

// 4. Enable debugger
$configurator->enableDebugger($p['logDir']);

// 5. Create class autoloader
$configurator->createRobotLoader()
	->addDirectory($p['appsDir'])
	->addDirectory($p['libsDir'])
	->register();

// 6. Create environment container
$configurator->addConfig($p['config'], FALSE);
$container = $configurator->createContainer();

/www/index.php

<?php

require __DIR__ . '/../bootstrap.php';

$container->application->run();

/console

#!/usr/bin/php
<?php

require __DIR__ . '/bootstrap.php';

$container->console->run();

/tests

#!/usr/bin/php
<?php

require __DIR__ . '/bootstrap.php';

$container->tests->run();

/scripts/some-quick-and-dirty-script.php

<?php

require __DIR__ . '/../bootstrap.php';

// do what the fuck you want...
$container->someService->doSomething();

echo 'Done.';
David Grudl
Nette Core | 8147
+
0
-

To už spíš:

bootstrap.php:

<?php

...
return $configurator->createContainer();

a index.php:

<?php
$container = require __DIR__ . '/../bootstrap.php';
$container->application->run();

Nicméně to už je krůček k tomu udělat třídu Bootstrap a pak mít v index.php

<?php
require __DIR__ . '/../bootstrap.php';

$container = Bootstrap::boot();
$container->application->run();
Morlok
Člen | 26
+
0
-

Tiež som potreboval potreboval nejak „manipulovat“ s boostrapom, teda niekedy toho nacitat viac, niekedy menej, a hlavne znovupouzitelnost.

Napadlo ma taketo riesenie:
Spravit si „Bootstraper“ ktory bude schopny registrovat rozne „Bootstrap triedy“, ktore budu implementovat potrebnu logiku volanu podla potreby. Ta logika su vpodstate nejake init-funckie,
ktore mozu vracat „resource“ (configurator, application,… cokolvek co init-funcke vrati).
Napr kod:

<?php
// boostrap.php
$bootstraper = new Miwo\Bootstrap\Bootstraper();
$bootstraper->addBoostrap(new SystemBootstrap());
$bootstraper->addBoostrap(new MyAppBootstrap());
$bootstraper->configurator;			// init configurator
$bootstraper->loader->register();		// init robot loader
$bootstraper->custom;				// init custom

// index.php
require APP_DIR . '/bootstrap.php';
$bootstraper->application->run();	// Bootstrap application

// SystemBootstrap.php
class SystemBootstrap extends Bootstrap {
	protected function initConfigurator() {
		$configurator = new Nette\Config\Configurator;
		$configurator->setTempDirectory(TEMP_DIR);
		$configurator->enableDebugger(LOG_DIR);
		return $configurator; // resource
	}

	protected function initLoader() {
		/* @var $configurator Nette\Config\Configurator */
		$configurator = $this->getResource('configurator');
		$robotLoader = $configurator->createRobotLoader();
		$robotLoader->addDirectory(APP_DIR);
		$robotLoader->addDirectory(LIBS_DIR);
		return $robotLoader; // resource
	}

	protected function initCustom() {
		$this->boostrap('router');
		$this->boostrap('cokolvek');
	}
}
?>

Init metody su hladane v registrovanych Bootstrapoch, mozu byt pretazene podla potreby…
Pripadne je mozne pozadovany „resource“ pozadovat priamo v parametri a nie je nutne volanie metody getResource()

<?php
	protected function initLoader(Nette\Config\Configurator $configurator) {
                $robotLoader = $configurator->createRobotLoader();
                $robotLoader->addDirectory(APP_DIR);
                $robotLoader->addDirectory(LIBS_DIR);
                return $robotLoader; // resource
        }
?>

Podla vas je to zbytocne? alebo takato technika moze mat potencial?

Editoval Morlok (31. 3. 2012 9:51)

Patrik Votoček
Člen | 2221
+
0
-

podle mě je to zbytečné protože toho samého lze dosáhnout pomocí Nette\Config\CompilerExtension

Morlok
Člen | 26
+
0
-

Patrik Votoček napsal(a):

podle mě je to zbytečné protože toho samého lze dosáhnout pomocí Nette\Config\CompilerExtension

Zalezi od problemu, mozno konkretne to nastavenie co som uviedol sa da previest do CompilerExtension alebo config.neonu, ale ked potrebujem podla rozneho nastavenia inicializovat configurator, dynamicky pridavat CompilerExtension-s a mat to nakodene v bootstrape sa mi vobec nepaci.

Filip Procházka
Moderator | 4668
+
0
-

Jedno slovo: over-engeneering.

Už já toho mám možná zbytečně moc, ale zase to je krásně ohebné.