phpcodeigniterdoctrine-ormsymfony

CodeIgniter Models vs Symfony/Doctrine Models


Background:

I have build my web application using CodeIgniter because it was the only framework I could grasp easily enough to get going quickly. Now seeing the unbelievably advanced functionality of symfony and the PSR standards I am hyped to get into it all.

Dialemma

I am not sure how to approach the model layer with symfony/doctrine. As I understand it: doctrine generates an entity class for a database table like so...

enter image description here

This class contains a bunch of setter/getter functions.

My mental block at the moment is that I don't understand how I am supposed to add to functionality to my model layer.

To understand where I am coming from take a look at a typical CodeIgniter Model that I am currently working with. This one handles discount coupons.

<?php

/**
 * This class handles all coupon codes
 */

class Coupon_Model extends CI_Model
{
    /**
     * gets a specific coupon
     * @param string $coupon_code
     * @return obj
     */
    public function getCoupon($coupon_code)
    {
        $this->db->where('coupon_code', $coupon_code);
        $query = $this->db->get('coupons');
        return $query->row();
    }

    /**
     * gets all coupons associated with a course
     * @param int $course_id
     * @return array
     */
    public function getCourseCoupons($course_id)
    {
        $this->db->where('course_id', $course_id);
        $query = $this->db->get('coupons');
        return $query->result();
    }

    /**
     * generates a string of 10 random alphanumeric numbers
     * @return string
     */
    public function generateCouponCode()
    {
        return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
    }

    /**
     * creates a new active coupon
     * @param array $data
     * @param string $coupon_code
     * @return bool
     */
    public function createCoupon($data, $coupon_code = null)
    {
        if ($coupon_code !== '') {
            $data['coupon_code'] = $coupon_code;
        } else {
            $data['coupon_code'] = $this->generateCouponCode();
        }

        return $this->db->insert('coupons', $data);
    }

    /**
     * checks if a coupon is valid
     * @param string $coupon_code
     * @param int    $course_id
     * @return bool
     */
    public function checkCoupon($coupon_code, $course_id = null)
    {
        $this->db->where('coupon_code', $coupon_code);
        $query = $this->db->get('coupons');
        $coupon = $query->row();

        // if coupon code exists
        if ($coupon === null) {
            return false;
        }

        // if coupon is for the right course
        if ($coupon->course_id !== $course_id && $course_id !== null) {
            return false;
        }

        // if coupon code has not expired
        if ($coupon->expiry_date <= $this->Time_Model->getCarbonNow()->timestamp) {
            return false;
        }
        return true;
    }

    /**
     * deletes a coupon record
     * @param int coupon_id
     * @return bool
     */
    public function deleteCoupon($coupon_id)
    {
        $this->db->where('coupon_id', $coupon_id);
        return $this->db->delete('coupons');
    }

    /**
     * applys the coupon discount
     * @param int   $price
     * @param float $discount (percentage)
     */
    public function applyDiscount($price, $discount)
    {
        $price = $price - (($discount / 100) * $price);
        return $price;
    }
}

As you can see it is pretty straight forward, if I wanted to add functionality I would literally just create a new function.

To use this model I would simply load it on the Controller like this:

$this->model->load('coupons/Coupon_Model');
$this->Coupon_Model->getCoupon($coupon_code);

Simple, done and dusted... unfortunately I am not sure how to implement this sort of functionality with symfony/doctrine.

Will I need to create a new class separate from the entity and add extra functionality to this class? Or should I add more functions to the entity class?

Take for example my simple function which generates the coupon code:

    /**
     * generates a string of 10 random alphanumeric numbers
     * @return string
     */
    public function generateCouponCode()
    {
        return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
    }

Where would be the best place to put this function? Under AppBundle/models/coupons?

I have clearly picked up bad habits from CodeIgniter and have a feeling that I am approaching this the wrong way.


Solution

  • Symfony + Doctrine ORM comes with a lot of the default needs for the replacement of CodeIgniter models by using the EntityManager within your Controller(s).

    For example

    namespace AppBundle\Controller;
    
    use Symfony\Component\HttpFoundation\Request;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    
    class DefaultController extends Controller
    {
    
        /**
         * @Route("/{id}/show", name="app_show", defaults={"id" = 1})
         */
        public function showAction(Request $request, $id)
        {
            $em = $this->getDoctrine()->getManager();
            if (!$coupon = $em->find('AppBundle:Coupon', $id)) {
                throw new NotFoundException('Unknown Coupon Specified');
            }
            
            //see below to see how this was implemented
            $similarCoupons = $em->getRepository('AppBundle:Coupon')
                                 ->filterCourse($coupon->course);
    
            return $this->render('AppBundle:template.twig', [
                 'coupon' => $coupon,
                 'similarCoupons' => $similarCoupons
            ]);
        }
    
        /**
         * @Route("/new", name="app_new")
         */
        public function newAction(Request $request)
        {
           //use Symfony Form Component instead
           $em = $this->getDoctrine()->getManager();
           $coupon = new \AppBundle\Entity\Coupon();
           //__construct calls $coupon->generateCouponCode();
    
           $coupon->setName($request->get('name'));
           $em->persist($coupon);
           $em->flush();
          
           return $this->redirectToRoute('app_show', ['id' => $coupon->getId()]);
        }
     
        //...
    }
    

    You want to specify the functionality you want each entity to have when working with it from within the Entity class. That it becomes available without needing to revisit the repository, since an Entity should never be aware of the EntityManager. In effect, each Entity can be considered their own models.

    For example $coupon->generateCouponCode(); or $this->generateCouponCode() from within the entity.

    Otherwise you would use a Repository of your Doctrine Database Entity(ies) to add more complex functionality.

    // /src/AppBundle/Entity/Coupon.php
    
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity(repository="CouponRepository")
     */
    class Coupon 
    {
       /**
        * @var integer
        * @ORM\Column(name="id", type="integer", nullable=false)
        * @ORM\Id
        * @ORM\GeneratedValue(strategy="IDENTITY")
        */
       private $id;
    
       /**
        * @var string
        * @ORM\Column(name="name", type="string", length=50)
        */
       private $name;
    
       /**
        * @var string
        * @ORM\Column(name="coupon_code", type="string", length=10)
        */
       private $couponCode;
    
       /**
        * @var Course
        * @ORM\ManyToOne(targetEntity="Course", inversedBy="coupons")
        * @ORM\JoinColumn(name="course", referencedColumnName="id")
        */
       private $course;
    
       //...
    
       public function __construct()
       {
           //optionally create code when persisting a new database entry by using LifeCycleCallbacks or a Listener instead of this line.
           $this->couponCode = $this->generateCouponCode();
       }
    
       //...
    
       /**
        * generates a string of 10 random alphanumeric numbers
        * @return string
        */
       public function generateCouponCode()
       {
           return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
       }
    }
    

    Then your custom queries would go into your Repository.

    // /src/AppBundle/Entity/CouponRepository.php
    
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\EntityRepository;
    
    class CouponRepository extends EntityRepository
    {
        /**
         * filters a collection of Coupons that matches the supplied Course
         * @param Course $course
         * @return array|Coupons[]
         */
        public function filterCourse(Course $course)
        {
            $qb = $this->createQueryBuilder('c');
            $expr = $qb->expr();
            $qb->where($expr->eq('c.course', ':course'))
                ->setParameter('course', $course);
    
            return $qb->getQuery()->getResult();
        }
     
    }  
    

    Additionally you can filter collections of an association (Foreign Key) reference within your entity.

    namespace AppBundle\Entity;
    
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Criteria;
    
    //...
    class Course
    {
        //...
      
       /**
        * @var ArrayCollection|Coupon[]
        * @ORM\OneToMany(targetEntity="Coupon", mappedBy="course")
        */
       private $coupons;
    
       public function __construct()
       {
           $this->coupons  = new ArrayCollection();
       }
    
       /**
        * @return ArrayCollection|Coupon[]
        */
       public function getCoupons()
       {
          return $this->coupons;
       }
    
       /**
        * @param string $name
        * @return \Doctrine\Common\Collections\Collection|Coupon[]
        */
       public function getCouponsByName($name)
       {
          $criteria = Criteria::create();
          $expr = $criteria::expr();
       
          return $this->coupons->matching($criteria->where($expr->eq('name', $name)));
       }
    }