What I need
I must build a module that allows input of very simple pay stubs. The classes representing this are PayStub
and Detail
, which are POPO and persistent. Moreover, the Detail
s must be balanced against the Fee
s, which in turn is a class that belongs to a Tariff
class. So, the Detail
has information about the Fee
that is being paid.
To make things slightly more complex, the Fee
the user must pay depends directly on what EducationLevel
the Enrolment
has, which belongs to a Student
, which also belongs to a Person
.
What I have
The PayStub
has:
- Pay Stub Number
- Payment Date
- Payment Method
- Observations
And the Detail
has:
- Fee
- Cheque, if any
- Paid amount
- Scholarship percentage
The Fee
has:
- Number of fee
- Price
Which belongs to a Tariff
class:
- Total amount
- Year
- Education Level
The issue
The whole problem comes when I build the form.
PayStubType
:
class PayStubType extends AbstractType
{
//I need to pass the years and the DAOs because I need to use them later
public function __construct(School $c, DAOPerson $dao, $years = array(), DAOFee $dc)
{
$this->c = $c;
$this->dao = $dao;
$this->years = $years;
$this->dc = $dc;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$sch = $this->c;
$std = $this->dao->getEnroledStudents($sch)[0];
$builder->add('student', 'entity', array(
'label' => 'Student',
'class' => 'System\SchoolsBundle\Person\Person',
'mapped' => false,
'query_builder' => $this->dao->getEnroledStudents($sch, true)
))->add('payDate', 'datetime', array(
'label' => 'Payment Date',
'widget' => 'single_text',
'input' => 'datetime',
'format' => 'dd/MM/yyyy'
))->add('payMethod', 'choice', array(
'label' => 'Payment Method',
'choices' => EPaymentMethod::$arrPayMethods
))->add('stubNumber', 'text', array(
'label' => 'Pay Stub No.',
'required' => false
))->add('observation', 'textarea', array(
'label' => 'Observations',
'max_length' => 1024,
'required' => false
));
$years = $this->years;
$dc = $this->dc;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($sch, $std, $years, $dc) {
$form = $event->getForm();
$data = $event->getData();
$stdRole = $std->getInfoContainer()->getRole('STUDENT');
$form->add('details', 'collection', array(
'type' => new DetailType($sch, $std, $years, $dc),
'label' => false,
'allow_add' => true,
'by_reference' => false
));
});
public function getName()
{
return 'paystubtype';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'System\SchoolsBundle\Payments\PayStub'
));
}
private $c;
private $dao;
private $years;
private $dc;
}
DetailType
:
class DetailType extends AbstractType
{
public function __construct(School $c, Student $al, $years = array(), DAOFee $dc)
{
$this->c = $c;
$this->al = $al;
$this->years = array_reverse($years, true);
$this->dc = $dc;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$sch = $this->c;
$list = array(); //List of scholarship percentages
for ($i=0; $i<=100; $i++) {
$list[(string)($i/100)] = $i."%";
}
$pref = min($cole->getSchoolYear(), array_values($this->years)[0]);
$builder->add('ct', 'choice', array(
'label' => false,
'mapped' => false,
'choices' => Fee::$arrFees //A list of possible fees. The only possible values are the enrolment price and one fee per school month. Read after the code for a longer explanation about how this works.
))->add('year', 'choice', array(
'mapped' => false,
'label' => false,
'choices' => $this->years, //Years that have tariffs registered
'preferred_choices' => array($pref) //The minimum between the current school year and the last year where tariffs were registered
))->add('cheque', 'entity', array(
'label' => false,
'class' => 'System\SchoolsBundle\Payments\Cheque',
'property' => 'numberAndBank',
'required' => false,
'empty_value' => 'Select Cheque',
'query_builder' => function(EntityRepository $er) use ($sch) {
return $er->createQueryBuilder('u')
->where('u.school = ?1')
->orderBy('u.number')
->setParameter(1, $sch);
}
))->add('amount', 'text', array(
'label' => false,
))->add('scholarshipPerc', 'choice', array(
'label' => false,
'choices' => $list
));
// From here on, it gets blurry. Read below for more.
}
public function getName()
{
return 'detailtype';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'System\SchoolsBundle\Payments\Detail'
));
}
private $c;
private $al;
private $years;
private $dc;
}
Regarding the fee choice field, I did not use an entity type because it would involve an AJAX query where, each time I update the student or the year, I would have to reload the prototype for the collection type, load the new fees for the year, and add extremely complex events which I am not sure that would work properly. So I decided to use a generic list and do the processing later.
Here's where things get complex, though. I was playing around with the events and decided a PRE_SUBMIT
event would be able to catch the year and fee, so I just query for it and add it to the object. However, the event does not deal with the mapped data type, so I have to pass this new information to another event and I decided this was the way to do it:
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) use ($sch, $al) {
$form = $event->getForm();
$data = $event->getData();
if (array_key_exists('ct', $data) == true && array_key_exists('year', $data) == true) {
$et = $al->getEnrolmentBySchool($sch, $data['year'])->getEducationLevel();
$year = $data['year'];
$feeNumber = $data['ct'];
$form->add('fee', 'entity', array(
'label' => false,
'class' => 'System\SchoolsBundle\Payments\Fee',
'property' => 'feeName',
'query_builder' => function(EntityRepository $er) use ($et, $year, $feeNumber) {
return $er->createQueryBuilder('u')
->innerJoin('u.tariff', 'a')
->innerJoin('a.edType', 'et')
->where('et = ?1')
->andWhere('a.year = ?2')
->andWhere('u.feeNumber = ?3')
->orderBy('u.feeNumber')
->setParameter(1, $et)
->setParameter(2, $year)
->setParameter(3, $feeNumber);
}
));
}
});
I was thinking I could just receive the data later, map it by hand, and discard it (i.e. remove it from the form). However, the POST_SUBMIT
event does not receive the object properly, neither in the form (null value) or the object (same issue).
Am I approaching this problem correctly? Is there a cleaner way to do this, or can I just solve this right in this way?