Model binding pro Nette formuláře

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

Poslední dobou jsem ve škole dělal něco v ASP.net MVC 2 a taky si četl dokumentaci Symfony2. A zalíbila se mi věc, která se jmenuje model binding. Znamená to, že data z formuláře se rovnou sypou do nějaké entity. Potom pracuji již s naplněnou entitou.

Sám pro sebe jsem si něco takového implementoval, ale není to dostatečně obecné, aby se to dalo zveřejnit.

Malý příklad:

Formulářový model. V jednoduchých případech např. Doctrine entita, ve složitějších vlastní třída.

class FormModel
{
	/** @validation:NotBlank */
	public $name;

	/**
	 * @validation:NotBlank
	 * @validation:Email(message="E-mail rozbitej")
	 */
	public $email = "@"; // defaultní hodnota

	private $date;

	public function __construct()
	{
		$this->date = new DateTime; // defaultní hodnota
	}

	public function setDate(DateTime $date)
	{
		$this->date = $date;
	}

	public function getDate()
	{
		return $this->date;
	}
}

Továrnička formuláře:

protected function createComponentForm()
{
	$form = new AppForm;

	$form->bindModel(new FormModel);
	$form->setValidator($this->getService("Validator")); // možná by to mohl umět sám od sebe

	// políčka vyjadřující datové typy - něco takového se do Nette 2.0 chystá, pokud vím
	$this->addText("name");
	$this->addEmail("email");
	$this->addDatePicker("date");

	$form->onSubmit[] = function ($form) {
		ulozit($form->getModel());
		// flashmessage, redirect
	};
}
jtousek
Člen | 951
+
0
-

Ta služba Validator je třída ze Symfony?

A jinak ano, líbí se mi to. V případě entit Doctrine 2 provázání formuláře přímo s entitou bije do očí a také mám v úmyslu to implementovat. Jen jsem si říkal že počkám na nové Nette formuláře než se do toho pustím.

//EDIT: Ale dát to obecně do frameworku, s tím už bych trochu váhal – ne každý používá Doctrine. Měl jsem spíše v úmyslu udělat to mimo. Pokud to bude přímo ve frameworku, nevadí mi to. (s rozumným API)

Editoval jtousek (26. 11. 2010 15:04)

mkoubik
Člen | 728
+
0
-

Taky jsem zkoušel vytvářet si různé entitní třídy, ale postupem času jsem zjistil, že u menších projektů je nejjednodušší prostě vzít pole z $form->values a skrz model ho nasypat do dibi query.

romansklenar
Člen | 655
+
0
-

Takovýto postup jde použít jen u entit, které nemájí žádné asociace. S těmi přicházejí už další komplikace a chtělo by na to myslet, pokud se bude nějaký takový doplněk tvořit.

jtousek
Člen | 951
+
0
-

@romansklenar: V případě asociací už by na to asi musely myslet i samotné prvky formuláře – což není nereálné a měl jsem v úmyslu si takové prvky udělat (kupříkladu select na výběr kategorie).

Honza Marek
Člen | 1664
+
0
-

jtousek

Ta služba Validator je třída ze Symfony?

Mohla by být.

jtousek

Ale dát to obecně do frameworku, s tím už bych trochu váhal – ne každý používá Doctrine.

mkoubik

u menších projektů je nejjednodušší prostě vzít pole z $form->values

Může to existovat s klasickým způsobem s getValues vedle sebe. Pokud budu chtít mít výstupem formuláře nějakou chytrou třídu místo pole, použiju binding.

romansklenar

Takovýto postup jde použít jen u entit, které nemájí žádné asociace.

Jsou dva způsoby jak to řešit.

  1. Vlastní formulářový prvek (CategorySelectBox, …).
  2. Někde něčemu zaregistrovat, že pro typ BaseEntity má provést $value->getId() a podobně.
srigi
Nette Blogger | 558
+
0
-

No parada, v Ostrave na PS som toto prezentoval, ze framework Yii to ma uz od zaciatku. Dostalo sa mi chladnej odozvy, ze je to moc previazane a pod. Teraz sa to ako bumerang vracia, plno ludi stale hlada riesenie – Entita, Formular, jednotna validacia. Pridam len malu ukazku toho riesenia v Yii.

Model (AR classa)

<?php
class User extends CActiveRecord
{
    /**
     * @var integer $id
     * @var string $email
     * @var string $password
     * @var string $name
     * @var string $role
     * @var integer $company_id
     */

    /** @var string */
    public passwordRepeat; // for password updates (virtual, not in DB)

    /**
     * @return Users
     */
    public static function model($className=__CLASS__) {
        return parent::model($className);
    }

    public function rules() {
        return array(
            array('email, name, password, passwordRepeat', 'required', 'on'=>'register'),
            array('email, name',                           'required', 'on'=>'update'),
            array('email, name, password, passwordRepeat', 'required', 'on'=>'changePasswd'),
            array('email',                                 'length', 'max'=>45),
            array('email',                                 'unique'),
            array('email',                                 'email'),
            array('password',                              'length', 'max'=>40),
            array('password',                              'compare', 'compareAttribute'=>'passwordRepeat', 'on'=>'register, changePasswd'),
            array('passwordRepeat',                        'length', 'max'=>40),
            array('name',                                  'length', 'max'=>80),
            array('role',                                  'length', 'max'=>20),
            array('role',                                  'match', 'pattern'=>'/admin|member/', 'on'=>'save'),
            array('company_id',                            'numerical', 'integerOnly'=>true),
        );
    }

    public function relations() {
        return array(
            'ratings' => array(self::HAS_MANY, 'Ratings', 'user_id'),
            'company' => array(self::BELONGS_TO, 'Companies', 'company_id'),
        );
    }

    public function attributeLabels() {
        return array(
            'email'    => 'Email',
            'password' => 'Password',
            'name'     => 'Name',
        );
    }
}
?>

Controller (spracovanie formu)

public function actionUpdateuser()
{
    if (!isset($_GET['id'])) {
        $this->redirect(array('staff'));
    }

    $model = User::model()->with('company')->findByPk($_GET['id']);

    if (!$model || ($model->company_id != Yii::app()->user->companyId)) {
        $this->redirect(array('staff'));
    }

    if (isset($_POST['User'])) {
        $_POST['User']['id'] = Yii::app()->user->id; // User cannot change ID via form
        $_POST['User']['role'] = Yii::app()->user->role; // User cannot change role via form
        $model->attributes = $_POST['Users'];

        if (empty($model->password)) {
            $model->setScenario('update');
        } else {
            $model->setScenario('changePasswd');
        }

        if ($model->validate()) {
            if (isset($model->password)) {
                $model->password = sha1($model->password.Yii::app()->params['dbsalt']);
            }
            try {
                $model->save(false);
            } catch (CDbException $e) {
                $this->render('updateuser.error');
                return;
            }

            $this->redirect(array('updateuser', 'id'=>$_GET['id']));
        }
    }

    $model->password = $model->passwordRepeat = ''; // clean password fields
    $this->render('updateuser', array('model'=>$model));
}

Sablona

<?= CHtml::beginForm(); ?>

    <?= CHtml::errorSummary($model); ?>

    <?= CHtml::activeLabel($model, 'name'); ?>
    <?= CHtml::activeTextField($model, 'name'); ?>

    <?= CHtml::activeLabel($model, 'email'); ?>
    <?= CHtml::activeTextField($model, 'email'); ?>

    <?= CHtml::activeLabel($model, 'password'); ?>
    <?= CHtml::activePasswordField($model, 'password'); ?>

    <?= CHtml::activeLabel($model, 'passwordRepeat'); ?>
    <?= CHtml::activePasswordField($model, 'passwordRepeat'); ?>

    <?= CHTML::submitButton('save'); ?>

<?= echo CHtml::endForm(); ?>

Vsetko je to krasne previazane (Formular, DB Row), ma to jednotne validacne pravidla a co sa mimoriadne paci (a v Nette netrpezlivo ocakavam) je moznost definovat typ formularoveho policka az v sablone, nezavisle na type Entitnej property.

Editoval srigi (26. 11. 2010 21:13)

Filip Procházka
Moderator | 4668
+
0
-

srigi: vypadá to že to funguje, byť to definování vypadá vskutku hrozně :)

Editoval HosipLan (27. 11. 2010 10:18)