Schematic ve spojení s Nette

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

Ahoj kolegové,

všiml jsem si, že balíček Schematic (repo, dokumentace, představení na Poslední Sobotě) začalo pár lidí používat a napadlo mě, že bych sem mohl dát pár tipů na jeho využití ve spojení s Nette Frameworkem. Doufám, že to pro někoho bude inspirativní a usnadní mu to život. :)

Editoval Tharos (29. 7. 2017 23:40)

Tharos
Člen | 1030
+
-2
-

Spojení s parametry DI kontejneru

Za užitečný use-case považuji možnost namapovat si na Schematicový záznam parametry aplikace přítomné v DI kontejneru. Mějme následující neon konfiguraci:

parameters:
	contactEmail: "someone@gmail.com"
	defaultDateTimeFormat: "Y-m-d H:i:s"
	anonymousPostsAllowed: false

Snadno ji můžeme namapovat na záznam a zaregistrovat do DI kontejneru například takto:

/**
 * @property-read string $contactEmail
 * @property-read string $defaultDateTimeFormat
 * @property-read bool $anonymousPostsAllowed
 */
class Parameters extends Schematic\Entry
{

	/**
	 * @param Container $container
	 */
	public function __construct(Container $container)
	{
		parent::__construct($container->getParameters());
	}

}

Tuto třídu pak stačí jen zaregistrovat mezi služby:

services:
	- Parameters

A voalá, kdekoliv v aplikaci budete potřebovat konfiguraci aplikace, stačí si jen vyžádat instanci Parameters a DI už se postará o zbytek:

class Posts
{

	/**
	 * @var Parameters
	 */
	private $parameters;


	/**
	 * @param Parameters $parameters
	 */
	public function __construct(Parameters $parameters)
	{
		$this->parameters = $parameters;
	}


	/**
	 * @param PostParameters $postParameters
	 */
	public function createAnonymousPost(PostParameters $postParameters)
	{
		if (!$this->parameters->anonymousPostsAllowed) {
			// ...
		}
	}

}

Pokud máte konfiguraci s více úrovněmi zanoření, vyloženě na ruku vám půjdou Schematicové asociace.

Editoval Tharos (29. 7. 2017 23:47)

Tharos
Člen | 1030
+
+2
-

Spojení s Nette formuláři

Za velmi užitečný use-case považuji možnost předávat hodnoty z formuláře k dalšímu zpracování v podobě jednoduchých DTO:

/**
 * @property-read string|NULL $author
 * @property-read string $subject
 * @property-read string $body
 */
class PostParameters extends Schematic\Entry
{
}

Takové DTO pak můžeme použít v Closure obsluhující formulář a také v API nějaké návazné služby:

/**
 * @return Form
 */
protected function createComponentPostForm()
{
	$form = $this->formFactory->create();

	// $form->addText...

	$form->onSuccess[] = function (Form $form) {
		$values = $form->getValues();

		try {
			$this->posts->createAnonymousPost(new PostParameters([
				'author' => $values->author,
				'subject' => $values->subject,
				'body' => $values->body,
			]);

			// $this->redirect...

		} catch (PostException $e) {
			// $form->addError...
		}
	};

	return $form;
}

Pokud by vám nevadilo mít DTO natvrdo provázané se strukturou formuláře, šlo by to zapsat ještě stručněji:

$this->posts->createAnonymousPost(
	new PostParameters($form->getValues($asArray = true)
);

Pracovat s takovýmito DTO namísto s asociativními poli je doslova návykové… Je to mnohem pohodlnější a IDE tomu rozumí: najednou začne napovídat, upozorňovat při přístupu k neexistující property atp…

Editoval Tharos (30. 7. 2017 7:59)

Tharos
Člen | 1030
+
+1
-

Spojení se šablonami

Třída EntryViewer je primárně navržená k inicializaci obsahu šablon, viz dokumentace. Chtěl bych trochu víc rozvézt, jak ji v praxi používám…

Typicky to začíná jako jednorázová záležitost:

$this->template->article = EntryViewer::viewEntry($article, function (Article $article) {
	return [
		'id' => $article->id,
		'title' => $article->title,
		'author' => EntryViewer::viewEntry($article->author, function (Author $author) {
			return [
				'id' => $author->id,
				'name' => $author->name,
			];
		}),
	];
});

Jakmile ale vznikne potřeba takový článek předávat do šablony na více místech (různé pohledy, komponenty…), vyčlením si konverzi do samostatné služby:

class ArticleConverter extends Object
{

	/**
	 * @param Article $article
	 * @return array
	 */
	public function convertArticleToArray(Article $article): array
	{
		return [
			'id' => $article->id,
			'title' => $article->title,
		];
	}

}
$this->template->article = EntryViewer::viewEntry($article, function (Article $article) {
	return $this->articleConverter->convertArticleToArray($article);
});

A teď se možná ptáte, kam se poděl autor. :) Mám ve zvyku neřešit v jedné konverzní třídě více asociovaných záznamů. Ať už si to poskládá podle potřeby ten, kdo to celé volá… Takto by vypadal výsledek:

class AuthorConverter extends Object
{

	/**
	 * @param Author $author
	 * @return array
	 */
	public function convertAuthorToArray(Author $author): array
	{
		return [
			'id' => $author->id,
			'name' => $author->name,
		];
	}

}
$this->template->article = EntryViewer::viewEntry($article, function (Article $article) {
	$convertedArticle = $this->articleConverter->convertArticleToArray($article);

	$convertedArticle['author'] = EntryViewer::viewEntry($article->author, function (Author $author) {
			return $this->authorConverter->convertAuthorToArray($author);
		})
	];

	return $convertedArticle;
});

Takto si přesně vždy na míru poskládám data, která v šabloně potřebuji. S provázáním šablon s konvertory dokážu žít, protože tyto konvertory používám právě jen ve spojení se šablonami a při refaktoringu mi IDE hned ukáže, kde se daný konvertor používá a v jakých šablonách se tedy pracuje s poli, které vrací.


Vypěstoval jsem si návyk takto šablony inicializovat a jsem s tím maximálně spokojený. Popravdě už si nedovedu dost dobře představit, že bych do šablony poslal nějaký doménový objekt (nedebože třeba entitu) a tam volal nějaké její metody…

Editoval Tharos (2. 8. 2017 17:32)