<file_sep># Doctrine Extensions Rateable

This repository contains the rateable extension for Doctrine 2. This allows to
rate your doctrine entities easily.


## Use

Each rate is a tuple consisting of a resource id, a user id and a score.


### Implement the DoctrineExtensions\Rateable\Rateable interface.

First, your entity must implement the `DoctrineExtensions\Rateable\Rateable` interface.
Five methods in your entity must be written:

 * `getRateableId()`
 * `getRateableType()`
 * `getRatingVotes()`
 * `setRatingVotes($number)`
 * `getRatingTotal()`
 * `setRatingTotal($number)`

Example:

    namespace MyProject;
    use DoctrineExtensions\Rateable\Rateable;

    /**
     * @Entity
     */
    class Article implements Rateable
    {
        /**
         * @Id
         * @GeneratedValue
         * @Column(type="integer")
         */
        protected $id;

        /**
         * @Column(name="rating_votes", type="integer")
         */
        protected $ratingVotes = 0;

        /**
         * @Column(name="rating_total", type="integer")
         */
        protected $ratingTotal = 0;


        public function getId()
        {
            return $this->id;
        }

        public function getResourceId()
        {
            return 'article-'.$this->getId();
        }

        public function getRatingVotes()
        {
            return $this->ratingVotes;
        }

        public function setRatingVotes($number)
        {
            $this->ratingVotes = $number;
        }

        public function getRatingTotal()
        {
            return $this->ratingTotal;
        }

        public function setRatingTotal($number)
        {
            $this->ratingTotal = $number;
        }
    }


### Implement the DoctrineExtensions\Rateable\Reviewer interface.

Second, your user entity must implement `DoctrineExtensions\Rateable\Reviewer` interface.
Four methods are needed:

 * `getReviewerId()`
 * `canAddRate(Rateable $resource)`
 * `canChangeRate(Rateable $resource)`
 * `canRemoveRate(Rateable $resource)`

Example:

    namespace MyProject;
    use DoctrineExtensions\Rateable\Rateable;
    use DoctrineExtensions\Rateable\Reviewer;

    /**
     * @Entity
     */
    class User implements Reviewer
    {
        /**
         * @Id
         * @GeneratedValue
         * @Column(type="integer")
         */
        protected $id;

        public function getId()
        {
            return $this->id;
        }

        public function getReviewerId()
        {
            return $this->getId();
        }

        public function canAddRate(Rateable $resource)
        {
            return true;
        }

        public function canChangeRate(Rateable $resource)
        {
            return true;
        }

        public function canRemoveRate(Rateable $resource)
        {
            return false;
        }
    }


### Setup Doctrine

Finally, you need to setup doctrine for register metadata directory and register RatingListener.


First, register the metadata directory of this package.

    $config = new \Doctrine\ORM\Configuration();
    // ...
    $driverImpl = new \Doctrine\ORM\Mapping\Driver\XmlDriver(array('/path/to/doctrine-extensions-rateable/metadata'));
    $config->setMetadataDriverImpl($driverImpl);

or with `DriverChain`:

    $driverImpl = new \Doctrine\ORM\Mapping\Driver\DriverChain();
    // ...
    $driverImpl->addDriver(new \Doctrine\ORM\Mapping\Driver\XmlDriver('/path/to/doctrine-extensions-rateable/metadata'), 'DoctrineExtensions\\Rateable\\Entity');


Then, register the RatingListener.

    // $this->em = EntityManager::create($connection, $config);
    // ...

    $this->ratingManager = new RatingManager($this->em);
    $this->em->getEventManager()->addEventSubscriber(new RatingListener($this->ratingManager));


### Using RatingManager

Now, you can use RatingManager.

    // Add a new rate..
    $this->ratingManager->addRate($resource, $user, 4);

    // Change my rate..
    $this->ratingManager->changeRate($resource, $user, 2);

    // Remove my rate..
    try {
        $this->ratingManager->removeRate($resource, $user);
    } catch (PermissionDeniedException $e) {
        echo 'Oh, no permission to remove my rate!';
    }


    // Compute resource rating score...
    $this->ratingManager->getRatingScore($resource); // will return 2


### Exceptions

Rateable extension can throw four different exceptions:

 * `InvalidRateScoreException` when your rate score is not between 1 and 5
 * `NotFoundRateException` when your try to change or remove an non existant rate
 * `PermissionDeniedException` when user have not permission to do this action
 * `ResourceAlreadyRatedException` when user have already rate the resource and you try to add new one

<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable\Exception;

class InvalidRateScoreException extends \Exception
{
    public function __construct($minValue, $maxValue, $code = null)
    {
        parent::__construct(
            sprintf('Rate score must be between %d and %d', $minValue, $maxValue),
            $code
        );
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable;

/**
 * Reviewer is the interface that user entity must implement.
 *
 * @author <NAME> <<EMAIL>>
 */
interface Reviewer
{
    /**
     * Returns unique identifiant
     *
     * @return integer An unique identifiant
     */
    function getReviewerId();

    /**
     * Check if user can add a rate for the resource
     *
     * @return Boolean TRUE if user can add a rate, FALSE otherwise.
     */
    function canAddRate(Rateable $resource);

    /**
     * Check if user can change his rate for the resource
     *
     * @return Boolean TRUE if user can change his rate, FALSE otherwise.
     */
    function canChangeRate(Rateable $resource);

    /**
     * Check if user can remove his rate for the resource
     *
     * @return Boolean TRUE if user can remove his rate, FALSE otherwise.
     */
    function canRemoveRate(Rateable $resource);
}
<file_sep><?php

namespace Tests\DoctrineExtensions\Rateable\Fixtures;

use DoctrineExtensions\Rateable\Rateable;
use DoctrineExtensions\Rateable\Reviewer;

/**
 * @Entity
 */
class User implements Reviewer
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="integer")
     */
    public $id = 0;

    /**
     * @Column(name="username", type="string", length=50)
     */
    public $username;

    public $canAddRate = true;
    public $canChangeRate = true;
    public $canRemoveRate = true;

    public function getId()
    {
        return $this->id;
    }


    public function getReviewerId()
    {
        return $this->getId();
    }

    public function canAddRate(Rateable $resource)
    {
        return $this->canAddRate;
    }

    public function canChangeRate(Rateable $resource)
    {
        return $this->canChangeRate;
    }

    public function canRemoveRate(Rateable $resource)
    {
        return $this->canRemoveRate;
    }
}
<file_sep><?php

namespace Tests\DoctrineExtensions\Rateable\Fixtures;

use DoctrineExtensions\Rateable\Entity\Rate as BaseRate;

/**
 * @Entity
 */
class Rate extends BaseRate
{
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

require_once __DIR__.'/Fixtures/Article.php';
require_once __DIR__.'/Fixtures/User.php';
require_once __DIR__.'/Fixtures/Rate.php';

use DoctrineExtensions\Rateable\RatingManager;
use DoctrineExtensions\Rateable\RatingListener;
use Tests\DoctrineExtensions\Rateable\Fixtures\User;
use Tests\DoctrineExtensions\Rateable\Fixtures\Article;

class RatingManagerTest extends \PHPUnit_Framework_TestCase
{
    protected $em;
    protected $manager;
    protected $article;

    public function setUp()
    {
        $config = new \Doctrine\ORM\Configuration();
        $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
        $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
        $config->setProxyDir(__DIR__ . '/Proxy');
        $config->setProxyNamespace('DoctrineExtensions\Rateable\Proxies');
        //$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver());

        $driverImpl = new \Doctrine\ORM\Mapping\Driver\DriverChain();
        $driverImpl->addDriver(new \Doctrine\ORM\Mapping\Driver\XmlDriver(__DIR__.'/../../../metadata'), 'DoctrineExtensions\\Rateable\\Entity');
        $driverImpl->addDriver($config->newDefaultAnnotationDriver(), 'Tests\\DoctrineExtensions\\Rateable\\Fixtures');
        $config->setMetadataDriverImpl($driverImpl);

        $this->em = \Doctrine\ORM\EntityManager::create(
            array('driver' => 'pdo_sqlite', 'memory' => true),
            $config
        );

        $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
        $schemaTool->dropSchema(array());
        $schemaTool->createSchema(array(
            $this->em->getClassMetadata('DoctrineExtensions\\Rateable\\Entity\\Rate'),
            $this->em->getClassMetadata('Tests\\DoctrineExtensions\\Rateable\\Fixtures\\Article'),
            $this->em->getClassMetadata('Tests\\DoctrineExtensions\\Rateable\\Fixtures\\User'),
        ));

        $article1 = new Article();
        $article1->title = 'Unit Testing';
        $article1->body = 'My first article about unit testing';
        $this->em->persist($article1);

        $this->article = new Article();
        $this->article->title = 'Unit Testing #2';
        $this->article->body = 'My second article about unit testing';
        $this->em->persist($this->article);

        $user1 = new User();
        $user1->username = 'Fabien';
        $this->em->persist($user1);

        $user2 = new User();
        $user2->username = 'Fabien2';
        $this->em->persist($user2);

        $this->em->flush();

        $this->manager = new RatingManager($this->em);
        $this->em->getEventManager()->addEventSubscriber(new RatingListener($this->manager));

        $this->manager->addRate($article1, $user1, 5);
        $this->manager->addRate($this->article, $user1, 3);
        $this->manager->addRate($this->article, $user2, 2);
    }

    public function testConstructor()
    {
        $manager = new RatingManager($this->em);
        $this->assertInstanceOf('DoctrineExtensions\Rateable\RatingManager', $manager);

        $class = 'Tests\DoctrineExtensions\Rateable\Fixtures\Rate';
        $manager = new RatingManager($this->em, $class);
        $this->assertEquals($class, $manager->getRateClass());
    }
    
    public function testSetMultiRating()
    {
        $this->manager->setMultiRating(true);
        $this->assertEquals(true, $this->manager->getMultiRating());
        $this->assertEquals(86400, $this->manager->getTimeBeforeNewRating());
        
        $this->manager->setMultiRating(false);
        $this->assertEquals(false, $this->manager->getMultiRating());
        $this->assertEquals(86400, $this->manager->getTimeBeforeNewRating());
        
        $this->manager->setMultiRating(true, 3600);
        $this->assertEquals(3600, $this->manager->getTimeBeforeNewRating());
    }

    public function testAddRate()
    {
        $user = new User();
        $user->id = 123;

        $this->assertEquals(2, $this->article->getRatingVotes());
        $this->assertEquals(5, $this->article->getRatingTotal());

        $this->manager->addRate($this->article, $user, 4);
        $this->assertEquals(3, $this->article->getRatingVotes());
        $this->assertEquals(9, $this->article->getRatingTotal());

        $rate = $this->manager->findRate($this->article, $user);
        $this->assertEquals($this->article->getRateableId(), $rate->getResourceId());
        $this->assertEquals($this->article->getRateableType(), $rate->getResourceType());
        $this->assertEquals(4, $rate->getScore());
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\PermissionDeniedException
     */
    public function testAddRateWithoutPermission()
    {
        $user = new User();
        $user->id = 456;
        $user->canAddRate = false;

        $this->manager->addRate($this->article, $user, 2);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\InvalidRateScoreException
     */
    public function testAddRateWithScoreLessThanOne()
    {
        $this->manager->addRate(new Article(), new User(), 0);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\InvalidRateScoreException
     */
    public function testAddRateWithScoreMoreThanFive()
    {
        $this->manager->addRate(new Article(), new User(), 6);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\ResourceAlreadyRatedException
     */
    public function testAddRateAgain()
    {
        $user = new User();
        $user->id = 123;

        $this->manager->addRate($this->article, $user, 4);
        $this->manager->addRate($this->article, $user, 4);
    }
    
    public function testAddRateAgainWithMultiRatingNoWait()
    {
        $user = new User();
        $user->id = 951;

        // First Test unlimited re-rate (no wait)
        $this->manager->setMultiRating(true, 0);
        
        $this->manager->addRate($this->article, $user, 3);
        $this->assertEquals(3, $this->article->getRatingVotes());
        $this->manager->addRate($this->article, $user, 3);
        $this->assertEquals(4, $this->article->getRatingVotes());
    }
    
    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\ResourceAlreadyRatedInPeriodException
     */
    public function testAddRateAgainWithMultiRatingWait()
    {
        $user = new User();
        $user->id = 951;
        
        // Then Test Mandatory waiting fail
        $this->manager->setMultiRating(true, 3600);
        $this->manager->addRate($this->article, $user, 3);
        $this->manager->addRate($this->article, $user, 3);
    }
    
    public function testAddRateAgainWithMultiRatingWaitOk()
    {
        $user = new User();
        $user->id = 851;
        
        // Then Test Mandatory waiting
        $this->manager->setMultiRating(true, 3600);
        $rate = $this->manager->addRate($this->article, $user, 3);
        $rate->setCreatedAt(new \DateTime('now-1 day'));
        $this->em->persist($rate);
        $this->em->flush();
        
        $this->manager->addRate($this->article, $user, 3);
    }


    public function testChangeRate()
    {
        $user = new User();
        $user->id = 123;

        $this->manager->addRate($this->article, $user, 4);
        $this->assertEquals(4, $this->manager->findRate($this->article, $user)->getScore());
        $this->assertEquals(9, $this->article->getRatingTotal());

        sleep(1);
        $this->manager->changeRate($this->article, $user, 2);
        $this->assertEquals(3, $this->article->getRatingVotes());
        $this->assertEquals(7, $this->article->getRatingTotal());

        $rate = $this->manager->findRate($this->article, $user);
        $this->assertEquals(2, $rate->getScore());
        $this->assertEquals(new \DateTime('now'), $rate->getUpdatedAt());
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\PermissionDeniedException
     */
    public function testChangeRateWithoutPermission()
    {
        $user = new User();
        $user->id = 456;
        $user->canChangeRate = false;

        $this->manager->changeRate($this->article, $user, 2);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\InvalidRateScoreException
     */
    public function testChangeRateWithScoreLessThanOne()
    {
        $this->manager->changeRate(new Article(), new User(), 0);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\InvalidRateScoreException
     */
    public function testChangeRateWithScoreMoreThanFive()
    {
        $this->manager->changeRate(new Article(), new User(), 6);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\NotFoundRateException
     */
    public function testChangeRateWithInvalidRate()
    {
        $user = new User();
        $user->id = 123;

        $article = new Article();
        $article->id = 456;

        $this->manager->changeRate($article, $user, 2);
    }


    public function testRemoveRate()
    {
        $user = new User();
        $user->id = 123;

        $this->manager->addRate($this->article, $user, 4);
        $this->assertEquals(4, $this->manager->findRate($this->article, $user)->getScore());
        $this->assertEquals(3, $this->article->getRatingVotes());

        $this->manager->removeRate($this->article, $user);
        $this->assertNull($this->manager->findRate($this->article, $user));
        $this->assertEquals(2, $this->article->getRatingVotes());
        $this->assertEquals(5, $this->article->getRatingTotal());
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\PermissionDeniedException
     */
    public function testRemoveRateWithoutPermission()
    {
        $user = new User();
        $user->id = 456;
        $user->canRemoveRate = false;

        $this->manager->removeRate($this->article, $user, 2);
    }

    /**
     * @expectedException DoctrineExtensions\Rateable\Exception\NotFoundRateException
     */
    public function testRemoveRateWithInvalidRate()
    {
        $user = new User();
        $user->id = 123;

        $article = new Article();
        $article->id = 456;

        $this->manager->removeRate($article, $user);
    }


    public function testComputeRating()
    {
        $this->article->ratingVotes = 10;
        $this->article->ratingTotal = 25;

        $this->manager->computeRating($this->article);
        $this->assertEquals(2, $this->article->getRatingVotes());
        $this->assertEquals(5, $this->article->getRatingTotal());
    }

    public function testGetRatingScore()
    {
        $this->assertEquals(3., $this->manager->getRatingScore($this->article));
        $this->assertEquals(2.5, $this->manager->getRatingScore($this->article, 1));

        $this->article->ratingVotes = 0;
        $this->article->ratingTotal = 0;
        $this->assertEquals(0, $this->manager->getRatingScore($this->article));
    }

    public function testIsValidRateScore()
    {
        $this->assertTrue($this->manager->isValidRateScore(1));
        $this->assertTrue($this->manager->isValidRateScore(5));
        $this->assertFalse($this->manager->isValidRateScore(0));
        $this->assertFalse($this->manager->isValidRateScore(6));
    }

    public function testDeleteResource()
    {
        $this->assertEquals(2, sizeof($this->manager->findRatesForResource($this->article)));

        $article = clone $this->article;
        $this->em->remove($this->article);
        $this->em->flush();

        $this->assertEquals(0, sizeof($this->manager->findRatesForResource($article)));
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable;

use Doctrine\ORM\EntityManager;

/**
 * RatingManager.
 *
 * @author <NAME> <<EMAIL>>
 * @author Bat <<EMAIL>>
 */
class RatingManager
{
    protected $em;
    protected $minRateScore = 1;
    protected $maxRateScore = 5;
    
    /**
     * Set if a user can add rating more than once
     * @var boolean 
     */
    protected $multiRating  = false;
    
    /**
     * Time before a user can re-rate. Only in case of multiRating.
     * @var integer time in seconds 
     */
    protected $timeBeforeNewRating = 86400;

    
    
    public function __construct(EntityManager $em, $class = null, $minRateScore = 1, $maxRateScore = 5)
    {
        $this->em = $em;
        $this->class = $class ?: 'DoctrineExtensions\Rateable\Entity\Rate';
        $this->minRateScore = $minRateScore;
        $this->maxRateScore = $maxRateScore;
    }
    
    /**
     * Set the permission to vote more than once
     * @param boolean $multiRating
     * @param integer $timeBeforeNewRating 
     */
    public function setMultiRating($multiRating, $timeBeforeNewRating = null)
    {
        $this->multiRating = $multiRating;
        if (!is_null($timeBeforeNewRating)) $this->timeBeforeNewRating = $timeBeforeNewRating;
    }
    
    public function getMultiRating()
    {
        return $this->multiRating;
    }
    
    public function getTimeBeforeNewRating()
    {
        return $this->timeBeforeNewRating;
    }

    /**
     * Adds a new rate.
     *
     * @param Rateable  $resource   The resource object
     * @param Reviewer  $reviewer   The reviewer object
     * @param integer   $rateScore  The rate score
     *
     * @return void
     */
    public function addRate(Rateable $resource, Reviewer $reviewer, $rateScore, $save=true)
    {
        if (!$reviewer->canAddRate($resource)) {
            throw new Exception\PermissionDeniedException('This reviewer cannot add a rate for this resource');
        }

        if (!$this->isValidRateScore($rateScore)) {
            throw new Exception\InvalidRateScoreException($this->minRateScore, $this->maxRateScore);
        }

        if ((!$this->multiRating || $this->timeBeforeNewRating > 0) && $lastRate = $this->findRate($resource, $reviewer)) {
            if (!$this->multiRating) {
              throw new Exception\ResourceAlreadyRatedException('The reviewer has already rated this resource');
            }
            elseif( $lastRate->getCreatedAt()->getTimestamp() >  time()-$this->getTimeBeforeNewRating()) {
              throw new Exception\ResourceAlreadyRatedInPeriodException('The reviewer must wait before re-rate this resource');
            }
        }

        $rate = $this->createRate();
        $rate->setResource($resource);
        $rate->setReviewer($reviewer);
        $rate->setScore($rateScore);

        $resource->setRatingVotes($resource->getRatingVotes() + 1);
        $resource->setRatingTotal($resource->getRatingTotal() + $rateScore);

        if ($save) {
            $this->saveEntity($rate);
            $this->saveEntity($resource);
        }

        return $rate;
    }

    /**
     * Changes score for an existant rate.
     * Warning : This method only works fine with single Rating
     * @todo : add method to remove All Rate, or on Rate by id
     *
     * @param Rateable  $resource   The resource object
     * @param Reviewer  $reviewer   The reviewer object
     * @param integer   $rateScore  The new rate score
     *
     * @return void
     */
    public function changeRate(Rateable $resource, Reviewer $reviewer, $rateScore)
    {
        if (!$reviewer->canChangeRate($resource)) {
            throw new Exception\PermissionDeniedException('This reviewer cannot change his rate for this resource');
        }

        if (!$this->isValidRateScore($rateScore)) {
            throw new Exception\InvalidRateScoreException($this->minRateScore, $this->maxRateScore);
        }

        if (!($rate = $this->findRate($resource, $reviewer))) {
            throw new Exception\NotFoundRateException('Unable to find rate object');
        }

        $resource->setRatingTotal($resource->getRatingTotal() - $rate->getScore());
        $resource->setRatingTotal($resource->getRatingTotal() + $rateScore);

        $rate->setScore($rateScore);
        $rate->setUpdatedAt(new \DateTime('now'));
        $this->saveEntity($rate);

        $this->saveEntity($resource);
    }

    /**
     * Removes an existant rate.
     * Warning : This method only works fine with single Rating
     * @todo : add method to remove All Rate, or on Rate by id
     *
     * @param Rateable  $resource   The resource object
     * @param Reviewer  $reviewer   The reviewer object
     *
     * @return void
     */
    public function removeRate(Rateable $resource, Reviewer $reviewer)
    {
        if (!$reviewer->canRemoveRate($resource)) {
            throw new Exception\PermissionDeniedException('This reviewer cannot remove his rate for this resource');
        }

        if (!($rate = $this->findRate($resource, $reviewer))) {
            throw new Exception\NotFoundRateException('Unable to find rate object');
        }

        $resource->setRatingVotes($resource->getRatingVotes() - 1);
        $resource->setRatingTotal($resource->getRatingTotal() - $rate->getScore());

        $this->em->remove($rate);
        $this->em->flush();

        $this->saveEntity($resource);
    }

    /**
     * Computes rating fields for an resource.
     *
     * @param Rateable  $resource   The resource object
     *
     * @return void
     */
    public function computeRating(Rateable $resource)
    {
        $votes = 0;
        $total = 0;

        foreach ($this->findRatesForResource($resource) as $rate) {
            $total += $rate->getScore();
            $votes++;
        }

        $resource->setRatingVotes($votes);
        $resource->setRatingTotal($total);
        $this->saveEntity($resource);
    }

    /**
     * Gets the rating score for a resource
     *
     * @param Rateable  $resource   The resource object
     * @param integer   $precision  Score precision
     *
     * @return void
     */
    public function getRatingScore(Rateable $resource, $precision=0)
    {
        if (!($resource->getRatingVotes() > 0)) {
            return 0;
        }

        return round($resource->getRatingTotal() / $resource->getRatingVotes(), $precision);
    }
    
    /**
     * @return integer min score allowed
     */
    public function getMinRateScore() 
    {
        return $this->minRateScore;
    }
    
    /**
     * @return integer max score allowed
     */
    public function getMaxScore()
    {
        return $this->maxRateScore;
    }
    
    /**
     * @return array range of allowed scores
     */
    public function getRange()
    {
        return range($this->minRateScore, $this->maxRateScore);
    }

    /**
     * Finds the last rate object for a couple resource/reviewer.
     *
     * @param Rateable  $resource   The resource object
     * @param Reviewer  $reviewer   The reviewer object
     *
     * @return null|Rate The rate object if exist, null otherwise
     */
    public function findRate(Rateable $resource, Reviewer $reviewer)
    {
        return $this->em
            ->getRepository($this->class)
            ->findOneBy(array(
                'resourceId'    => $resource->getRateableId(),
                'resourceType'  => $resource->getRateableType(),
                'reviewerId'    => $reviewer->getReviewerId(),
            ), array('createdAt' => 'DESC'))
        ;
    }
    
    /**
     * Finds rates collection for a couple resource/reviewer
     * 
     * @param Rateable $resource
     * @param Reviewer $reviewer
     * @return DoctrineCollection 
     */
    public function findRates(Rateable $resource, Reviewer $reviewer)
    {
        return $this->em
                ->getRepository($this->class)
                ->findBy(array(
                  'resourceId'    => $resource->getRateableId(),
                  'resourceType'  => $resource->getRateableType(),
                  'reviewerId'    => $reviewer->getReviewerId(),
                ), array('createdAt' => 'DESC'))
            ;
    }

    /**
     * Finds all rate objects for a resource.
     *
     * @param Rateable  $resource   The resource object
     *
     * @return Doctrine\Common\Collection
     */
    public function findRatesForResource(Rateable $resource)
    {
        return $this->em
            ->getRepository($this->class)
            ->findBy(array(
                'resourceId'    => $resource->getRateableId(),
                'resourceType'  => $resource->getRateableType(),
            ))
        ;
    }

    /**
     * Checks if a value is a valid rate score
     *
     * @param integer   $rateScore  The rate score
     *
     * @return Boolean TRUE if value is valid, FALSE otherwise
     */
    public function isValidRateScore($score)
    {
        return ($score >= $this->minRateScore
            and $score <= $this->maxRateScore);
    }

    /**
     * Returns the class name for Rate entity
     *
     * @return string
     */
    public function getRateClass()
    {
        return $this->class;
    }

    /**
     * Creates a new Rate object
     *
     * @return Rate
     */
    protected function createRate()
    {
        return new $this->class();
    }

    /**
     * Saves a Doctrine Entity
     *
     * @param mixed  $entity   The entity object
     *
     * @return mixed Entity object
     */
    protected function saveEntity($entity)
    {
        $this->em->persist($entity);
        $this->em->flush();

        return $entity;
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable\Exception;

class NotFoundRateException extends \Exception
{}
<file_sep><?php

namespace Tests\DoctrineExtensions\Rateable\Fixtures;

/**
 * @Entity
 */
class Article implements \DoctrineExtensions\Rateable\Rateable
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="integer")
     */
    public $id = 0;

    /**
     * @Column(name="title", type="string", length=50)
     */
    public $title;

    /**
     * @Column(name="body", type="string")
     */
    public $body;

    /**
     * @Column(name="rating_votes", type="integer")
     */
    public $ratingVotes = 0;

    /**
     * @Column(name="rating_total", type="integer")
     */
    public $ratingTotal = 0;


    public function getId()
    {
        return $this->id;
    }

    public function getRateableId()
    {
        return $this->getId();
    }
    
    public function getRateableType()
    {
        return 'Article';
    }

    public function getRatingVotes()
    {
        return $this->ratingVotes;
    }

    public function setRatingVotes($number)
    {
        $this->ratingVotes = $number;
    }

    public function getRatingTotal()
    {
        return $this->ratingTotal;
    }

    public function setRatingTotal($number)
    {
        $this->ratingTotal = $number;
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable\Entity;

use DoctrineExtensions\Rateable\Rateable;
use DoctrineExtensions\Rateable\Reviewer;

class Rate
{
    protected $id;

    protected $resourceId;
    protected $resourceType;
    
    protected $reviewerId;

    protected $score;

    protected $createdAt;
    protected $updatedAt;


    public function __construct()
    {
        $this->createdAt = new \DateTime('now');
        $this->updatedAt = new \DateTime('now');
    }

    public function getId()
    {
        return $this->id;
    }

    public function setResource(Rateable $resource)
    {
        $this->resourceId   = $resource->getRateableId();
        $this->resourceType = $resource->getRateableType();
    }

    public function getResourceId()
    {
        return $this->resourceId;
    }
    
    public function getResourceType()
    {
        return $this->resourceType;
    }

    public function setReviewer(Reviewer $reviewer)
    {
        $this->reviewerId = $reviewer->getReviewerId();
    }

    public function getReviewerId()
    {
        return $this->reviewerId;
    }

    public function setScore($score)
    {
        $this->score = $score;
    }

    public function getScore()
    {
        return $this->score;
    }

    public function setCreatedAt(\DateTime $date)
    {
        $this->createdAt = $date;
    }

    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    public function setUpdatedAt(\DateTime $date)
    {
        $this->updatedAt = $date;
    }

    public function getUpdatedAt()
    {
        return $this->updatedAt;
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\LifecycleEventArgs;

/**
 * RatingListener.
 *
 * @author <NAME> <<EMAIL>>
 */
class RatingListener implements EventSubscriber
{
    protected $manager;

    /**
     * Constructor
     *
     * @param ContainerInterface $container
     */
    public function __construct(RatingManager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * @see Doctrine\Common\EventSubscriber
     */
    public function getSubscribedEvents()
    {
        return array(Events::preRemove);
    }

    /**
     * @param LifecycleEventArgs $args
     */
    public function preRemove(LifecycleEventArgs $args)
    {
        if (($resource = $args->getEntity()) and $resource instanceof Rateable) {
            $em = $args->getEntityManager();

            foreach ($this->manager->findRatesForResource($resource) as $rate) {
                $em->remove($rate);
            }
        }
    }
}<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

require_once __DIR__.'/../Fixtures/Article.php';
require_once __DIR__.'/../Fixtures/User.php';

use DoctrineExtensions\Rateable\Rateable;
use DoctrineExtensions\Rateable\Entity\Rate;
use Tests\DoctrineExtensions\Rateable\Fixtures\User;


class RateTest extends \PHPUnit_Framework_TestCase
{
    public function testConstructor()
    {
        $rate = new Rate();
        $this->assertEquals(new \DateTime('now'), $rate->getCreatedAt());
        $this->assertEquals(new \DateTime('now'), $rate->getUpdatedAt());
    }

    /**
     * @covers DoctrineExtensions\Rateable\Entity\Rate::setResource
     * @covers DoctrineExtensions\Rateable\Entity\Rate::getResourceId
     */
    public function testSetGetResource()
    {
        $article = new \Tests\DoctrineExtensions\Rateable\Fixtures\Article();
        $article->id = 123;

        $rate = new Rate();
        $rate->setResource($article);

        $this->assertEquals($article->getRateableId(), $rate->getResourceId());
        $this->assertEquals($article->getRateableType(), $rate->getResourceType());
    }

    /**
     * @covers DoctrineExtensions\Rateable\Entity\Rate::setReviewer
     * @covers DoctrineExtensions\Rateable\Entity\Rate::getReviewerId
     */
    public function testSetGetReviewer()
    {
        $user = new User();
        $user->id = 345;

        $rate = new Rate();
        $rate->setReviewer($user);

        $this->assertEquals(345, $rate->getReviewerId());
    }
}
<file_sep><?php

/*
 * This file is part of the Doctrine Extensions Rateable package.
 * (c) 2011 <NAME> <<EMAIL>>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace DoctrineExtensions\Rateable;

/**
 * Rateable is the interface that rateable resource classes must implement.
 *
 * @author <NAME> <<EMAIL>>
 * @author Bat <<EMAIL>>
 */
interface Rateable
{
    /**
     * Get rateable identifier, typically id (getId())
     * return mixed
     */
    function getRateableId();
    
    /**
     * Get rateable Type, typically class name
     * @return string
     */
    function getRateableType();

    
    /**
     * Return number of votes
     * @return integer
     */
    function getRatingVotes();
    
    /**
     * Set number of votes
     * @param integer $number
     */
    function setRatingVotes($number);

    
    /**
     * Get rateable object score (total)
     * @return integer
     */
    function getRatingTotal();
    
    /**
     * Set rateable object score (total)
     * @param integer $number
     */
    function setRatingTotal($number);
    
    
    /**
     * Compute the average score of a resource
     * @return integer
     */
    function getAverageScore();
}
