About 2 years ago, I built my first Symfony2 project. The most recent version back then was 2.0.12 which I figured out from the now deprecated /deps file.
[symfony]
git=http://github.com/symfony/symfony.git
version=v2.0.12
This project was part of my internship where I was given a LAMP stack to deploy on. I chose Symfony because of the resemblance with ASP.NET MVC which I used on a previous project and enjoyed working with.
I used SVN before but I wasn't familiar with GIT. The project was under version control but the only version I found is the zip file I handed over at the end of my internship.
If this project requires further development, it would be a good idea to put
it under version control. The vendor folder can be excluded together with
the nbproject that somehow snuck into the zip file. Because 2.0 lacks
composer, the command to install vendors is different from what I'm used to
but the workflow stays the same: php bin/vendors install
I had to download the missing .gitignore file from the symfony-standard
repository. After running git init; git add .
in the root of the project,
further changes can be tracked.
After changing the /app/config/parameters.ini file, creating the database
and running php app/console doctrine:schema:update --force
, the project is
available in my local development environment. The Apache and PHP config that I
use for more recent projects seems to do the job, no tweaking required. Just to
be sure, I checked /web/config.php, it did not complain.
Clicking through the website I ran into the following errors:
- There are no questions!
- Can't find any question catergories!
Other pages just show a blank area where a list of options should appear. For example: when starting an assessment you should be able to pick an opleiding but none are presented.
There are 2 measures we can take to counter these issues.
First of all, showing sensible messages when data is missing to properly render views. Depending on the role of the user and their ability to add content, you can point them in the right direction and guide them through their first encounter with the application.
A second solution is seeding the project with the help of Doctrine fixtures. These fixtures can be run on command so they are extremely helpful if you mess up your data or you quickly need the dummy content.
The controllers in this project ended up being quite bulky. Looking back, they provide plenty of examples where improvements can be made.
Instead of defining forms inside the controller they can be defined in separate form classes that can directly map to existing model entities or view models. This way we can consolidate duplicate code in controllers and compose more elaborate forms by combining different form classes.
Another way of keeping controllers dry is creating custom repository classes to persist and retrieve entities.
The following controller action code looks for all assessments created between a begin and end date.
<?php
// src/OAT/OATBundle/Controller/StatisticsController.php
public function assessmentAction()
{
...
$categoryAssessments = $this->getDoctrine()
->getRepository('OATBundle:CategoryAssessment')
->createQueryBuilder('c')
->where("c.status = 1")
->andWhere("c.created <= :endDate")
->andWhere("c.created >= :startDate")
->setParameters(array('startDate' => $startDate, 'endDate' => $endDate))
->getQuery()
->getResult();
...
}
This Query can be moved to a repository class where we define it as a function.
<?php
// src/OAT/OATBundle/Entity/AssessmentRepository.php
class AssessmentRepository extends EntityRepository
{
public function findAllByTimespan($startDate, $endDate){
...
}
}
We can then replace the query in the controller action with the repository function.
<?php
// src/OAT/OATBundle/Controller/StatisticsController.php
public function assessmentAction()
{
...
$categoryAssessments = $this->getDoctrine()
->getRepository('OATBundle:CategoryAssessment')
->findAllByTimespan($startDate, $endDate);
...
}
The following controller action code takes a list of category groups, which can contain overlapping categories, matches them against a list of all the existing categories and builds a set with unique categories to create an assessment.
<?php
// src/OAT/OATBundle/Controller/AssessmentController.php newAction
$categories = Array();
$categoryGroups = $this->getDoctrine()
->getRepository('OATBundle:QuestionCategoryGroup')
->findAll();
$selectedGroups = Array();
foreach($categoryGroups as $categoryGroup)
{
if($request->request->get('category-group-'.$categoryGroup->getId())) {
$selectedGroups[] = $categoryGroup;
foreach($categoryGroup->getQuestionCategoryGroupMember() as $category)
{
if(!in_array($category->getQuestionCategory(), $categories)) {
$categories[] = $category->getQuestionCategory();
}
}
}
}
You don't want the repository call and http request logic to end up in the model. The following code will match the requested group IDs against existing category groups.
<?php
// src/OAT/OATBundle/Controller/AssessmentController.php
private function getRequestedCategoryGroupIds(Request $request) {
// Get all the posted variables
$postedVariables = $request->request->all();
// There's a post variable prefixed with 'category-group-' for each category group
// You can't filter on key so the array must be flipped.
$postedCategoryGroups = array_filter(array_flip($postedVariables), function($key) {
return strpos($key, 'category-group-') === 0;
});
// Strip the group IDs
$categoryGroupIds = array_map(function($value) {
return intval(str_replace("category-group-", "", $value));
}, $postedCategoryGroups);
return $categoryGroupIds;
}
public function newAction(Request $request) {
$categoryGroupIds = $this->getRequestedCategoryGroupIds($request);
// Find the matching group entities
$categoryGroups = $this->getDoctrine()
->getRepository('OATBundle:QuestionCategoryGroup')
->findById($categoryGroupIds);
...
}
Further down in the action, the category groups are added after the assessment object is created. Instead of just passing the groups to the constructor, all their categories were reduced to a set to use as parameter.
<?php
// src/OAT/OATBundle/Controller/AssessmentController.php
$assessment = new Assessment($categories);
$assessment->setCreated(new DateTime("now"));
foreach($selectedGroups as $group)
{
$assessment->addQuestionCategoryGroup($group);
}
You should be able to create an assessment by passing the groups and let the constructor deal with them. The model should decide if duplicate categories are allowed without involving the controller for any domain logic.
<?php
// src/OAT/OATBundle/Entity/Assessment.php
/**
* @param QuestionCategoryGroup[] $groups
*/
public function __construct(Array $groups)
{
...
foreach ($groups as $group) {
$this->addQuestionCategoryGroup($group);
}
...
}
public function addQuestionCategoryGroup(QuestionCategoryGroup $group)
{
$this->questionCategoryGroups[] = $group;
foreach($group->getQuestionCategoryGroupMembers() as $categoryGroupMember) {
$this->addQuestionCategory($categoryGroupMember->getQuestionCategory());
}
}
public function addQuestionCategory(QuestionCategory $category)
{
if(!in_array($this->getQuestionCategories(), $category) {
$this->questionCategories[] = $category;
};
}
Switching to Composer would make it easier to manage security updates. This requires updating Symfony to at least v2.1. Going for v2.3 will get you long-term support and makes sure you are only running stable versions of external dependencies. This project does not rely heavily on third-party bundles so upgrading should not be that hard by following the Symfony upgrade guide.
Javascript libraries are stored in the OAT bundle. This creates unneeded bloat in the repository and requires you to downloaded and add them manually.
For more recent projects I'm using Bower and it makes
developing with small javascript libraries so much easier. You can copy over a
list of dependencies from existing projects and running bower install
will
get the locally cached files or download them when needed. Assetic can still be
used to minify everything to a single file.