4

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 Details must be balanced against the Fees, 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?

yivi
  • 42,438
  • 18
  • 116
  • 138
Carlos Vergara
  • 3,592
  • 4
  • 31
  • 56
  • 9
    I feel that your question is to over complicated. What exactly are you trying to do? – Daniel Ribeiro Jun 18 '14 at 14:53
  • 5
    Figure out, where your problem is and then just post the according lines. We're not here to find your problem. – Gottlieb Notschnabel Jun 21 '14 at 02:31
  • So as I understand the problem is to calculate one of the form fields (fee) based on the values of other form fields (ed. type and tariff). It basically means that this calculated field is not directly controlled by a user hence should not be a form field or a part of your PayStubType/DetailType at all. – nikita2206 Dec 05 '14 at 09:13

1 Answers1

0

I would like to suggest a couple of consideration:
The first is about the code, I think the logic inside your Form starts to be a little bit complex, better avoid too many lines of codes in same class, so I would like to think about a refactor according to the SRP(Single Responsibility Principle).

The Second things is that I cannot really know if your approach is correct.
I could assume it is, and maybe the using of PRE_SUBMIT could facilitate the things, but I would like to pay more attention to your architecture.

What I mean is, we need to understand more about the Fee issue.
If you think that to get the value will consume time and also could slow down the submit process of the form (that has to be quick as possible), It might be worth to think about some alternatives:

  • Split the form in 2 subform (example 2 pages, step1 and step2);
  • restructure the model data, creating for example another table, or better a view or an Api that could fetch the value quickly;
  • Calculate the value later with a cron job.

Hope this point of view could be helpful.

nik.longstone
  • 244
  • 2
  • 8