A symfony beágyazott (embedded) form megoldásáról szóló kis szösszenet kerül ide.
A segítségével két, master-detail kapcsolatban álló tábla egyidejű módosítását lehet megvalósítani.

Itt is az első leírásban létrehozott sandbox projektünket fogjuk átszerkeszteni.

Tehát állítsuk be a config könyvtárban lévő adatbázisokhoz tartozó fájlokat (propel.ini és databases.yml) az előzőleg létrehozott sandbox adatbázisra. Majd szerkesszük meg a config/schema.yml fájlt:

[code language=”sql”]
propel:
_attributes:
package: lib.model
defaultIdMethod: native
osztaly:
_attributes: { phpName: Osztaly }
id: { phpName: Id, type: INTEGER, size: ’11’, primaryKey: true, autoIncrement: true, required: true }
kod: { phpName: Kod, type: CHAR, size: ‘1’, required: true }
evfolyam: { phpName: Evfolyam, type: INTEGER, size: ’11’, required: true }
tanulo:
_attributes: { phpName: Tanulo }
id: { phpName: Id, type: INTEGER, size: ’11’, primaryKey: true, autoIncrement: true, required: true }
name: { phpName: Name, type: VARCHAR, size: ‘255’, required: true }
szuletes: { phpName: Szuletes, type: DATE, required: true }
osztaly_id: { phpName: OsztalyId, type: INTEGER, size: ’11’, required: false, foreignTable: osztaly, foreignReference: id, onDelete: CASCADE, onUpdate: CASCADE }
_indexes: { category_id: [osztaly_id] }
[/code]

Majd futtassuk le az alábbi parancsokat is:
[code language=”bash”]
# hozzuk létre a formokat, filtereket, modeleket, és a táblákat is generáljuk le
# a kérdésre, miszerint kívánjuk-e hogy felülírja az adatbázisunkat válaszoljunk igennel (Y)
./symfony propel:build-all
# és a két táblánkhoz tartozó admin felületet készítsük el:
./symfony propel:generate-admin frontend osztaly
./symfony propel:generate-admin frontend tanulo
[/code]

Szükségünk lesz egy pluginra, a sfJqueryReloadedPluginra. A telepítése nem túl bonyolult. Ha az összes plugint engedélyeztük (a config/ProjectConfiguration.class.php fájlban), akkor elég bemásolni a plugin könyvtárba. Mivel a letöltött tömörített fájlban mindenféle kódszámot ír a könyvtárnév végére, így a könnyebb elérhetőség miatt használjuk inkább a sfJqueryReloadedPlugin könyvtárnevet.

A lib/model/Osztaly.php fájlt pedig egészítsük ki az alábbi metódussal
[code lang=”php”]
public function __toString()
{
return $this->evfolyam.’/’.$this->kod;
}
[/code]

Az apps/frontend/config/routing.yml fájlt is módosítsuk, hogy az osztályok listája jelenjen meg alapértelmezetten.
[code lang=”php”]
homepage:
url: /
param: { module: osztaly, action: index }
[/code]

Ezzel az alap projektünk elkészült. Lehet listázni, új osztályt felvinni és módosítani. Azonban az osztályhoz még nem tudunk tanulót rögzíteni. Ehhez módosítsuk a lib/form/OsztalyForm.class.php fájlt.
Ez egy hosszabb lélegzetvétel lesz, mert nem aprózom el 🙂

[code lang=”php”]
class OsztalyForm extends BaseOsztalyForm
{
/**
* a form konfiguralasa
* hozzaadjuk a tanulokat a formhoz
* hozzadjuk a hozzaadas gombot (Uj) letrehozo widgetet is.
*/
public function configure()
{
parent::setup();
$context = sfContext::getInstance();
$this->loadHelpers($context);

$tanulok = $this->getObject()->getTanulos();

//Egy ures formba eltaroljuk az embeddelt TanuloFormokat
$this->embedForm(‘tanulos’, new sfForm());
$count = 0;
foreach ($tanulok as $tanulo) {
$this->addTanulo($count, $tanulo);
$count ++;
}

// a hozzaadas gomb mint egy uj mezo kerul a formunkba
$this->widgetSchema[‘addEmbedform’] = new embedderFormWidget(array(
‘url’=>url_for(‘osztaly/getEmbedForm’),
‘html_element’ => ".sf_admin_form_field_tanulos table:first",
));
$this->widgetSchema->setLabel(‘addEmbedform’, false);
}

/**
* kulonbozo helperek betoltese
* Url helper az ajax hivashoz hasznalando helyes url megadasahoz
* jQueryReloadedPlugin jQuery helpere
* es a jQuery UI plugin javascript betoltese
*
* @param sfContext $context
*/
public function loadHelpers($context)
{
$context->getConfiguration()->loadHelpers(array(‘jQuery’,’Tag’,’Url’));
$context->getResponse()->addStyleSheet(sfConfig::get(‘sf_jquery_web_dir’, ‘/sfJqueryReloadedPlugin’) . "/css/ui-lightness/jquery-ui-1.8.2.custom.css");
jq_add_plugins_by_name(array(‘ui’));
}

/**
* A torles link a Tanulo mellett
*
* @param string $index
* @return string
*/
public function getEmbedFormLabel($index)
{
return jq_link_to_function(
‘<span class="ui-icon ui-icon-trash"></span>’,
"jQuery(this).parents(‘tr:first’).remove(); jQuery(‘#osztaly_tanulos_".$index."_id’).remove();",
array(‘confirm’ => ‘Sure???’));
}

/**
* A Tanulo hozzaadasa az Osztalyhoz
*
* Erre akkor is szuksegunk lesz, amikor egy uj tanulot veszunk fel
*
* @param string $num
* @param Tanulo $tanulo
*/
public function addTanulo($num, $tanulo = null)
{
if (!$tanulo)
{
$tanulo = new Tanulo();
}
$tanulo->setOsztaly($this->getObject());
$form = new TanuloForm($tanulo);
$form->getWidgetSchema()->offsetUnset(‘osztaly_id’);
$this->embeddedForms[‘tanulos’]->embedForm($num, $form);

// torles link
$label = $this->getEmbedFormLabel($num);
$this->embeddedForms[‘tanulos’]->widgetSchema->setLabel($num,$label);

$this->embedForm(‘tanulos’, $this->embeddedForms[‘tanulos’]);
}

/**
*
* Ha rogziteskor uj tanulot vettunk fel, akkor adjuk hozza az osztalyhoz
* Ha pedig toroltunk akkor tavolitsuk el a formbol
*/
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
if (isset($taintedValues[‘tanulos’]))
{
foreach($taintedValues[‘tanulos’] as $key => $tanulo)
{
if (!isset($this[‘tanulos’][$key]))
{
$this->addTanulo($key);
}
}
}

foreach ($this->embeddedForms[‘tanulos’] as $key=>$form)
{
if (!isset($taintedValues[‘tanulos’][$key]) && $key<>’_csrf_token’)
{
$this->getWidgetSchema()->offsetGet(‘tanulos’)->offsetUnset($key);
$this->getValidatorSchema()->offsetGet(‘tanulos’)->offsetUnset($key);
unset(
$taintedValues[‘tanulos’][$key],
$taintedFiles[‘tanulos’][$key]);
}
}

parent::bind($taintedValues, $taintedFiles);
}

/**
* Tanulonak beallitjuk az osztalyat
*
* Amikor egy uj osztallyal egyutt viszunk fel egy tanulot, akkor
* meg kell adnunk a tanulonak is, hogy melyik osztalyba tartozik.
* Raadasul eltavolitottuk a tanulo from osztaly_id mezojet, így minden
* egyeb esetben is szukseg van erre.
*
*/
public function saveEmbeddedForms($con = null, $forms = null)
{
$oszaly = $this->getObject();
if ($forms)
{
foreach ($forms as $form)
{
if ($form instanceof sfFormObject)
{
$form->getObject()->setOsztaly($oszaly);
}
}
}

return parent::saveEmbeddedForms($con, $forms);
}

/**
* Ezt az osztalyfuggvenyt bovitjuk. A kikeressuk a postolt adatokbol a tanulok
* azonositojat (id). Amelyik tanulo azonositoja nincs benne a kapott eredmenyben,
* toroljuk az osztalybol.
*/
public function updateObjectEmbeddedForms ($values, $forms = null)
{
// $values[‘tanulos’][][id] elemeket, ahol id>0
$ids = set::extract(‘/tanulos[id>0]/id’, $values);
if (!$forms)
{
$c = new Criteria();
$c->add(TanuloPeer::OSZTALY_ID, $this->getObject()->getId());
$c->add(TanuloPeer::ID, $ids, Criteria::NOT_IN);
TanuloPeer::doDelete($c);
}
return parent::updateObjectEmbeddedForms ($values, $forms);
}
}
[/code]

Három dolgot használtunk fel a formban, amit még el kell készítenünk.

Egy widget, ami létrehozza a tanuló hozzáadása linket. Ezt embedderFormWidget néven neveztem el és a lib/widget mappába tegyük. Ez a mappa még nem létezik, létre kell hozni. Ebbe hozuk létre a embedderFormWidget.class.php fájlt:

[code lang=”php”]
<?php
/**
* @author nova
* @version 1.0.0
*/
class embedderFormWidget extends sfWidgetForm
{
/**
* Configures the current widget.
*
* Available options:
*
* @param string html_element – az elem, amihez hozzafuzunk
* @param string url – az ajay keres url-je
*
* @see sfWidgetForm
*/
protected function configure($options = array(), $attributes = array())
{
$this->addOption(‘html_element’);
$this->addOption(‘url’);
parent::configure($options, $attributes);
}

/**
* @param string $name The element name
* @param string $value The date displayed in this widget
* @param array $attributes An array of HTML attributes to be merged with the default HTML attributes
* @param array $errors An array of errors for the field
*
* @return string An HTML tag string
*
* @see sfWidgetForm
*/
public function render($name, $value = null, $attributes = array(), $errors = array())
{
$attributes = $this->getAttributes();
$html =jq_link_to_function(
‘<span class="ui-icon ui-icon-circle-plus fright"></span><span class="fright">Új</span>’,
"jQuery.get(‘".$this->getOption(‘url’)."’,function(data){
jQuery(‘".$this->getOption(‘html_element’)."’).append(data);
});",
$attributes);
return $html;
}
}
[/code]

Aztán gondoskodnuk kell az ajax hívás válaszáról a szerver oldalon. Módosítsuk az apps/fronntedn/modules/osztaly/actions/actions.class.php fájlt az alábbi metódussal:
[code lang=”php”]
/**
* Visszaad egy új sort (widgetet) a tanulo formhoz
*
* @param sfWebRequest $request
*/
public function executeGetEmbedForm(sfWebRequest $request)
{
$form = $this->configuration->getForm();
$num = ‘tmp_’.time();
$form->addTanulo($num);

return $this->renderText($form->getWidget(‘tanulos’)->getWidget($num)->render($num));
}
[/code]

Ezen kívül még felhasználtam egy konkurens MVC rendszerben található igencsak hasznos osztályt, a
CakePHP set osztályát
Ezt persze módosítanom kellett, hogy szeresse a Symfony is. Ide tettem fel.

Letölthetővé tettem a teljes projektet is, innen.

Sajnos annyi energiám nem volt hogy lépésről lépésre építsem fel a projektet, remélem így is hasznosnak találja, aki elolvassa. Nekem meg jó jegyzet 🙂