First of all I want to state that I’m quite new to the Symfony Framework and that my English does not compare with a native, but hey, I’ll do my best.
At the moment I’m working on my first Symfony application what will be a toto for the upcoming World Championship soccer. A quite challenging application but fun and instructive.
The design presented me with a great challenge. As you can see in the picture above (which is in Dutch, sorry), we want multiple rows containing the matches of that group per row, where you can fill in your prediction of the result of that match.
The schema I’ve figured out is this one (just the relevant part) :
Groups: columns: id: { type: integer, primary: true, autoincrement: true } name: { type: string(100) } Team: columns: id: { type: integer, primary: true, autoincrement: true } group_id: { type: integer, notnull: true } name: { type: string(255) } flag: { type: string(255) } desription: { type: string(4000) } biography: { type: string(4000) } points: { type: integer(2) } relations: Groups: { local: group_id, foreign: id } Games: columns: id: { type: integer, primary: true, autoincrement: true } home_team_id: { type: integer, notnull: true } away_team_id: { type: integer, notnull: true } stadium_id: { type: integer } playing_time: { type: timestamp } desription: { type: string(3000) } relations: Stadium: { local: stadium_id, foreign: id, foreignAlias: Stadium } HomeTeam: { class: Team, local: home_team_id, foreign: id, foreignAlias: homeTeam } AwayTeam: { class: Team, local: away_team_id, foreign: id, foreignAlias: awayTeam } Predictions: actAs: [Timestampable] columns: id: { type: integer, primary: true, autoincrement: true } user_id: { type: integer(4) } game_id: { type: integer } home_score: { type: integer(2) } away_score: { type: integer(2) } real_home_score: { type: integer(2) } real_away_score: { type: integer(2) } relations: Profile: { local: user_id, foreign: user_id, foreignAlias: User } Games: { local: game_id, foreign: id, foreignAlias: Game }
Okay, now I’ve shown you the design we wanted and the schema I’ve used. Let me show the implementation I’ve used and the steps how I got to the result.
We want the user to predict the matches that are in the system so I want to show all the games with an embedded form of Prediction. But the problem here is that the 1:n relation is sorted out via the Predictions model, which to me is logical as you have one match with multiple predictions. Here the difficulty came. How do you couple a game to a prediction when there isn’t a relation yet, which is the case in a first prediction.
This is my solution:
I’ve created a new action in the Games controller called executePredict:
public function executePredict( sfWebRequest $request ) { $this->form = new GamesCollectionForm( ); }
Here we call GamesCollectionForm which collects all games in the system and creates a form for them.
class GamesCollectionForm extends BaseGamesForm { public function configure() { $this->useFields(array()); $q = Doctrine_Query::create()->from('Games'); $aGames = $q->execute(); $wrapperForm = new sfForm(); foreach( $aGames as $index => $game ) { $gameForm = new GamesPredictionForm( $game ); $gameForm->widgetSchema->setNameFormat('games[predictions][game_' . $index . '][%s]'); $wrapperForm->embedForm('game_' . $index, $gameForm); } $this->embedForm('predictions', $wrapperForm); } }
Okay, here I ran into multiple problems. The first one being that the collection form is an child of BaseGamesForm and therefore contains form widgets for adding a game, something I don’t want. So clean them all out by stating that we aren’t going to use any fields.
Next we collect all the games in the system and create an empty sfForm as wrapperform. In this wrapperform we’re going to embed instances of the GamePredictionForm. So for every game we create a GamesPredictionForm and set the right name format. This name format we need for the rendering and saving the forms.
Each form is embedded to the wrapper by setting its index prefixed with “game_”. Eventually the wrapperform gets embedded to the collection form.
The GamesPredictionForm looks like this:
class GamesPredictionForm extends BaseGamesForm { public function configure( ) { $q = Doctrine_Query::create()->from('Predictions')->where('user_id = ? ', sfContext::getInstance()->getUser()->getProfile()->getUserID())->andWhere('game_id = ?', $this->getObject()->getID()); $oPred = $q->fetchOne(); if(! $oPred instanceof Predictions ) { $oPred = new Predictions(); $this->getObject()->Game[] = $oPred; } $this->embedRelation('Game', 'PredictionsForm', array('user_id' => sfContext::getInstance()->getUser()->getProfile()->getUserID())); $this->widgetSchema['home_team_id'] = new sfWidgetFormInputHidden(); $this->widgetSchema['away_team_id'] = new sfWidgetFormInputHidden(); } }
What this class does is checking if there is a prediction of the user for this game. If this isn’t the case we create an empty Predictions object so the form renders a Prediction form. The Predictions model gets embedded with the game form and the id widgets are set hidden. This configuration of hidden inputs is needed for the customized rendering as I going to show later.
If we now render our form we got the result like below.
Nice, but not quite what we wanted. Besides that, any user now can edit the stadium, playing time or description. That’s not want we’d like to see, so lets start change the rendering of this form.
Before we change the rendering of the form I’d like to mention that at this moment it isn’t possible to save the form. If you’ll try to save now a database error is thrown stating that you try to enter null values in the games table. Which is correct, we’ve said that we don’t want to use any fields in the GamesCollectionForm. That works for the widgets, not for the save function. We need to modify that. My solution to this problem took me ages to find and I doubt that this is the best way to handle this.
What I’ve done is to override the doSave function of the GamesCollectionForm and checked if there’s any home_team_id been set. If not, the form is used for prediction by a user and we only want to save the embedded forms. In code it looks like this:
public function doSave( $con = null ) { if( $this->getObject()->getHomeTeamId() != null ) { } else { $this->updateObjectEmbeddedForms($this->taintedValues); $this->saveEmbeddedForms($con); return; } }
What took me so long to figure out is the updateObjectEmbeddedForms. It seems this just don’t happens with the bind function of the form which is called in the action. By that it mean, if the parent values are all null, the embedded forms doesn’t get bound to the form values. I did not tested this so it’s an assumption and not a fact.
public function executePredict( sfWebRequest $request ) { if($request->isMethod(sfRequest::POST) || $request->isMethod(sfRequest::PUT)) { $games = Doctrine::getTable('Games')->find(array($request->getParameter('id'))); $this->form = new GamesCollectionForm($games); $this->processForm($request, $this->form); } else { $this->form = new GamesCollectionForm( ); } } protected function processForm(sfWebRequest $request, sfForm $form) { $form->bind($request->getParameter($form->getName()), $request->getFiles($form->getName())); if ($form->isValid()) { $form->save(); $this->redirect('games/predict'); } }
Well, now we can save our form whether its new or just an update, it works and that’s a victory on its own.
Now the rendering of the form. Our goal is to get the rendering as suggested by the designers. Well I can tell a lot about this, but lets just start by showing the code and the end result of that code. This is the code of the template rendering:
<?php use_helper('Date'); ?> <h1>Voorspel de wedstrijden</h1> <form action="<?php echo url_for('games/predict') ?>" method="POST"> <table width="100%"> <?php echo $form['id'];?> <?php echo $form[$form->getCSRFFieldName() ]->render(); ?> <?php foreach( $form->getEmbeddedForm('predictions')->getEmbeddedForms() as $pred): ?> <tr> <td><?php echo format_datetime($pred->getObject()->getPlayingTime(), 'dd-MM-yyyy HH:mm'); ?></td> <td><?php echo "<img src='/uploads/flags/" . $pred->getObject()->getHomeTeam()->getFlag() . "' width=15 height=10/>"; echo $pred->getObject()->getHomeTeam(); ?></td> <td> - </td> <td><?php echo "<img src='/uploads/flags/" . $pred->getObject()->getAwayTeam()->getFlag() . "' width=15 height=10/>"; echo $pred->getObject()->getAwayTeam(); ?></td> <td><?php echo $pred->getObject()->getStadium()->getName(); ?></td> <td><?php echo $pred->getObject()->getStadium()->getCity(); ?></td> <td><?php echo $pred['Game'][0]['home_score']; ?></td> <td> - </td> <td><?php echo $pred['Game'][0]['away_score']; ?></td> <?php echo $pred['Game'][0]['id']; ?> <?php echo $pred['Game'][0]['user_id']; ?> <?php echo $pred['Game'][0]['game_id']; ?> <?php echo $pred['home_team_id']; ?> <?php echo $pred['away_team_id']; ?> </tr> <?php endforeach; ?> <tr> <td colspan="2"> <input type="submit" /> </td> </tr> </table> </form>
Which leads us to…
Hey! It looks like we’ve got it right. In essence this is just like the designers wishes. Well, what’s up with this form rendering…
First of all I start with rendering the hidden form ID tag and the hidden CSFR field. We need this for the binding and security of the form (I’m not sure of the binding part).
Then I want the embedded Predictions form which is the wrapperform containing all forms per match as an embedded form. Hence, the double getEmbeddedForms call. The $pred variable now holds an instance of the GamesPredictionForm class.
Per row we first render all the static match information by retrieving the object and outputting them. After that we render the prediction form elements which are stored in the Game property of the GamesPredictionForm. The game[0] holds the Predictions form which needs to be editable for the user.
That are all the visible fields, they are followed by a bunch of hidden fields so that the objects can be stored right. This information is processed by the bind and updateObject function and are mandatory.
This rendering will be done for every Game stored in the system. And the best thing is that it works!
But there’s a catch. It works and I’m glad with that, but I’m not happy with this solution. I can’t figure out if the model design is incorrect, if I use Symfony wrong or just do something other totally the wrong way around. Therefore I need you! If you got to here you probably have the same problem or a huge interest in Symfony ( or just me, but I’ll let that idea go by ).
Perhaps you can tell me what I can do better to prevent these kind of ‘hacks’ and adjustments. Help me to grow as developer and give your feedback. I appreciate it big time!
Thanks for your time so far!
Hi Paul,
thank your for this post, it helped me a lot!
I am currently working on something similar (no football, but the object relations are similar).
But you definetly should refactor your code by putting something like
Doctrine_Query::create()->from(‘Predictions’)->where(‘user_id = ? ‘, sfContext::getInstance()->getUser()->getProfile()->getUserID())->andWhere(‘game_id = ?’, $this->getObject()->getID());
into the corresponding model class! In this case it is PredictionsTable.class.php, i think.
That fits a lot better into the MVC pattern as the query stuff is done in the model, where it belongs.
One last thing:
You complained about the form extending BaseGamesForm which is forcing you to unset all widgets first.
You can avoid this workaround by simply building your custom form class which then extends BaseForm.
Arif
Hi Arif,
Glad this post helped you with you journey in Symfony.
About the refactoring, you’re absolutely right. But when I was figuring out this stuff I’ve changed the code so much, that the MVC principle got lost along the way. In the end result everything was refeactored the correct way 🙂
Extending the baseform is a very good idea! Thanks for your feedback!