Formuláře a jejich process služby

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

Dlouho mé formuláře vypadaly nějak takto:

class SettingForm extends \Sharezone\Application\UI\TemplateForm
{

	/**
	 * @var SettingFormProcess $settingFormProcess
	 */
	private $settingFormProcess;

	/**
	 * @param SettingFormProcess $settingFormProcess
	 */
	public function injectSettingFormProcess(SettingFormProcess $settingFormProcess)
	{
		$this->settingFormProcess = $settingFormProcess;
	}

	/**
	 * @param array $defaults
	 */
	public function __construct(array $defaults)
	{
		parent::__construct();

		$this->configure();
		$this->setDefaults($defaults);
		$this->onSuccess[] = $this->formSubmitted;
	}

	/**
	 * @param SettingForm $form
	 */
	public function formSubmitted(SettingForm $form)
	{
		try {
			$this->settingFormProcess->send($form->getValues());
			$this->presenter->flashMessage('Your profile has been updated.', 'success');
			$this->presenter->redirect('Stream:');
		} catch (\Nette\InvalidArgumentException $ex) {
			$form->addError($ex->getMessage());
		}
	}

	private function configure()
	{

		$this->addText('nick', 'Username:');

		//..
	}
}

v configu:

factories:
settingForm:
    create: \Sharezone\Components\Settings\SettingForm(%defaults%)
    implement: \Sharezone\Components\Settings\ISettingFormFactory
    parameters: [array defaults]

Po tom co se začalo diskutovat o tom, zda bude podpora inject* metod pro verzi 2.1 zachována, uvědomil jsem si, že to není možná úplně dobrý způsob.

Vše by bylo krásně vyřešeno, kdybych závislost předal v konstruktoru, ale přiznejme si, že předávat services závislosti formu přes kontruktor je trochu hloupé :)

Vzhledem k tomu, že ve své app používám kdyby/events napadlo mě jiné řešení.

Vytvořil jsem si FormListener, který se mi bude starat o události:

use Kdyby\Events\Subscriber;
use Nette\DI\Container;
use Nette\InvalidStateException;
use Nette\Object;
use Nette\Application\UI\Form;

class FormListener extends Object implements Subscriber
{

	/** @var \Nette\DI\Container */
	private $context;

	/**
	 * @param Container $container
	 */
	public function __construct(Container $container)
	{
		$this->context = $container;
	}

	/**
	 * @param Form $form
	 * @param      $class
	 * @throws \Nette\InvalidStateException
	 */
	public function onSuccessSend(Form $form, $class)
	{
		$service = $this->context->getByType($class);

		if($service instanceof IFormProcess) {
			$service->process($form);
		}else{
			throw new InvalidStateException($class . ' must be instance of Sharezone\Application\IFormProcess');
		}
	}

	/**
	 * Returns an array of events this subscriber wants to listen to.
	 *
	 * @return array
	 */
	function getSubscribedEvents()
	{
		return array(
			'Sharezone\Application\UI\Form::onSuccessSend',
			'Sharezone\Application\UI\TemplateForm::onSuccessSend',
		);
	}

přidáno do configu:

FormListener:
    class: \Sharezone\Application\FormListener
    tags: [kdyby.subscriber]

Když nově vytvořený Listener aplikuji na jakýkoliv form, může to vypadat nějak takto:

class AddCommentForm extends \Sharezone\Application\UI\TemplateForm
{

	/**
	 * @param $id
	 */
	public function __construct($id)
	{
		parent::__construct();
		$this->setId((int)$id);
		$this->configure();
		$this->attachProcessService();
	}

	private function configure()
	{
		$this->getElementPrototype()->id = 'addCommentForm';

		$this->addTextArea('content', 'Comment')
			->addRule(self::MAX_LENGTH, null, 500)
			->addRule(self::FILLED)
			->setAttribute('placeholder', 'Please enter your comment');

		$this->addSubmit('send', 'Submit');
	}

}

kde metoda attachProcessService je jen syntax sugar pro:

public function attachProcessService()
	{
		$self = $this; //php 5.3
		$this->onSuccess[] = function (Form $form) use ($self) {
			$self->onSuccessSend($form, get_called_class() . 'Process');
		};
	}

a konečně třída starající se o zpracování formu:

class AddCommentFormProcess extends Object implements IFormProcess
{

	/**
	 * @var \Sharezone\Model\Comments\CommentManager $commentManager
	 */
	private $commentManager;

	/**
	 * @param \Sharezone\Model\Comments\CommentManager $commentManager
	 */
	public function __construct(\Sharezone\Model\Comments\CommentManager $commentManager)
	{
		$this->commentManager = $commentManager;
	}

	/**
	 * @param Form $form
	 * @return mixed|void
	 */
	public function process(Form $form)
	{
		try {
			$this->commentManager->create($form->getValues());
			$form->presenter->flashMessage('Comment was successfully added.', 'success');
		} catch (\Nette\InvalidArgumentException $ex) {
			$form->addError($ex->getMessage());
		};
	}
}

Závislosti můžu krásně předávat konstruktorem tak, jak by se to mělo.

Nesmím zapomenout, že třídy Sharezone\Application\UI\Form a Sharezone\Application\UI\TemplateForm musí implementovat public property $onSuccessSend, tak aby fungovalo kdyby/events

Vím, že to není dokonalý způsob, ale je to takový jednoduchý způsob pro líné programátory :)

Jako největší nevýhodu výše popsaného postupu bych asi bral to, že v FormListeneru vytvářím závilost na Containeru.

Zajímá mě váš názor na tento postup.

A pokud máte vlastní best practies pro obsluhu formulářů tak, aby logika nebyla umístěna v presenteru, prosím podělte se o ni. Rád se něco přiučím!:-)

//Edit: Docela se mi zalíbil postup, který zmiňuje David zde: https://forum.nette.org/…nebo-tovarna ale trochu mi vadí, že nevyužiji factories a jejich implement parametru. Má někdo s tímto postupem zkušenosti?

Editoval sifik (17. 5. 2013 13:50)

Filip Procházka
Moderator | 4668
+
0
-

Vše by bylo krásně vyřešeno, kdybych závislost předal v konstruktoru, ale přiznejme si, že předávat services závislosti formu přes kontruktor je trochu hloupé :)

Není to ani trochu hloupé, ba dokonce velice chytré :)

Docela se mi zalíbil postup, který zmiňuje David, ale trochu mi vadí, že nevyužiji factories a jejich implement parametru. Má někdo s tímto postupem zkušenosti?

Tohle je také oblíbený způsob a není na něm nic špatného :)


Líbí se mi žes použil kdyby/events, ale nejsem si jist, jestli úplně nejlépe. Víš co mně se na tom tvém postupu nelíbí? :) Že formulář je pevně svázaný s „processorem“.

Mně by se líbilo takovéto použití (s trochou pomoci od kamaráda autowired)

protected function createComponentEditForm(
	IMyEditFormFactory $factory,
	ConcreteProcessor $processor)
{
	$form = $factory->create(); // vytvoří instanci formuláře generovanou továrničkou
	$processor->attach($form);
	$processor->onSuccess[] = function () {
		// měl bys přejít na PHP 5.4 ;)
		$this->flashMessage('Comment was successfully added.', 'success');
		$this->redirect('this');
	};
	return $form;
}

Nějaký abstraktní processor by mohl vypadat takto

abstract class Processor extends Nette\Object
{
	public function attach(UI\Form $form)
	{
		$form->onSuccess[] = $this->success;
		$form->onError[] = $this->error;
		$form->onValidate[] = $this->validate;
		return $this;
	}

	public function success(UI\Form $form)
	{
	}

	public function validate(UI\Form $form)
	{
	}

	public function error(UI\Form $form)
	{
	}
}

A konkrétní takto

class ConcreteProcessor extends Processor
{
	public $onSuccess = array();

	/** @var \Sharezone\Model\Comments\CommentManager $commentManager */
	private $commentManager;

	public function __construct(\Sharezone\Model\Comments\CommentManager $commentManager)
	{
		$this->commentManager = $commentManager;
	}

	public function success(UI\Form $form)
	{
		try {
			$this->commentManager->create($form->getValues());
			$this->onSuccess($form, $this);

		} catch (\Nette\InvalidArgumentException $ex) {
			$form->addError($ex->getMessage());
		};
	}

}

Co jsem tím získal?

  • formuláři můžu navázat libovolné množství procesorů, pokud si nebudou vadit navzájem
  • nový event pro každý procesor, zde například ConcreteProcessor::onSuccess, na který můžu navázat další listenery

Vím že například @Tharos měl zpracovaný hezký koncept těchto procesorů, ale nedaří se mi to dohledat, třeba si to přečte a ozve se :)

sifik
Člen | 27
+
0
-

Filip Procházka napsal(a):

Vše by bylo krásně vyřešeno, kdybych závislost předal v konstruktoru, ale přiznejme si, že předávat services závislosti formu přes kontruktor je trochu hloupé :)

Není to ani trochu hloupé, ba dokonce velice chytré :)

Hlavním argumentem, proč jsem to nazval za hloupé bylo to, že kdych chtěl services závislosti předávat konstruktorem, musel bych nejdříve services nějak injectnout do presenteru tam je předat create() metodě příslušné komponenty. Když budu hodně přehánět tak v jednom presenteru bud mít 3 komponenty, a každý bude potřebovat 3 services – to je 9 závilostí na presenter, což mi přijde moc (nepočítaje to, že sám presenter může mít vlastní závilosti). Nebo co dělám špatně? :-)

Ale co mě opravdu děsí, je definice factories v configu:

suggestFriends:
    create: \Sharezone\Components\Friends\Suggest\SuggestFriendsControl(%user%)
    implement: \Sharezone\Components\Friends\Suggest\ISuggestFriendsControlFactory
    parameters: [\Sharezone\Entity\User user]

Už aby si továrnčky dokázaly argumenty nadefinovat sami přes anotace :)

Postup, který si ukázal se mi velice líbí! A děkuji ti za něj!

Co ale moc nechápu a proto tě prosím o objasnění – jak je to s tím, že jsem nemusel registrovat žádný listener pro ConcreteProcessor::onSuccess? OnSuccess si bere kdyby/events globálně nastarost, nebo jak funguje tahle část?

A když už jsme to trochu naťukli, nedá mi to se nezeptat, jak jinak dostávat tedy závislosti do například do komponenty(potažmo do formu) tak, abych nezneužíval inject* metody?

Šaman
Člen | 2666
+
0
-

Tam, kde fungují inject* metody, tam funguje i autowire v konstruktoru. Inject metody se doporučují používat JEN v presenterech, protože předávat presenteru závislosti pomocí konstruktoru může vést k constructor hell. Jinde se doporučuje používat konstruktor, pokud nemáš velice dobrý důvod proč ho nepoužít.

Filip Procházka
Moderator | 4668
+
0
-

Proč se používají inject* jen presenterech

Už aby si továrnčky dokázaly argumenty nadefinovat sami přes anotace :)

To by bylo porušením inversion of control :) Ale můžeš sledovat tuhle issue.

Co ale moc nechápu a proto tě prosím o objasnění – jak je to s tím, že jsem nemusel registrovat žádný listener pro ConcreteProcessor::onSuccess? OnSuccess si bere kdyby/events globálně nastarost, nebo jak funguje tahle část?

Ale ten přece registruješ tady

$processor->onSuccess[] = function () {
	$this->flashMessage('Comment was successfully added.', 'success');
	$this->redirect('this');
};

Ale navíc máš nový event, na který můžeš navázat externí listenery :)

A když už jsme to trochu naťukli, nedá mi to se nezeptat, jak jinak dostávat tedy závislosti do například do komponenty(potažmo do formu) tak, abych nezneužíval inject* metody?

Přes konstruktor přece. https://doc.nette.org/…tion/factory

Tharos
Člen | 1030
+
0
-

Filip Procházka napsal(a):

Vím že například @Tharos měl zpracovaný hezký koncept těchto procesorů, ale nedaří se mi to dohledat, třeba si to přečte a ozve se :)

No, až taková sláva to nebyla… Popravdě momentálně se řídím jiným „paradigma“, takže bych tamten styl nerad někde propagoval. Nebyl špatný, ale řešil problémy, které jsem vlastně ani neměl, a byl zbytečně ukecaný… takže šel z domu. :)