diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f792900 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 5.3 + - 5.4 + +before_script: + - wget http://getcomposer.org/composer.phar -O ./composer.phar + - chmod +x composer.phar + - ./composer.phar install --prefer-dist + - gem install colored -v=1.2 + +script: phpunit \ No newline at end of file diff --git a/README.md b/README.md index 2f2693b..59581a2 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,14 @@ The API is very similar except for: Until I get to PEAR2, just use composer. +## WIP + +topics/doctrine + + * add a container for Doctrine2 ORM + * run CI testing on travis + ## TODO - * add a container for Doctrine2 (ORM or DBAL?) * add support for Swiftmailer * investigate better queues than RDBMS diff --git a/composer.json b/composer.json index 31123bb..9bccf03 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,9 @@ ], "require": { "pear-pear/MDB2": "2.5.0b5", + "pear-pear/MDB2_Driver_sqlite": "*", "pear-pear/Mail": "*", - "pear-pear/Mail_Mime": "*" + "pear-pear/Mail_Mime": "*", + "doctrine/orm": "2.4.x-dev" } } diff --git a/src/PEAR2/Mail/Queue/Container/doctrine2.php b/src/PEAR2/Mail/Queue/Container/doctrine2.php index c12cd7a..c8f045b 100644 --- a/src/PEAR2/Mail/Queue/Container/doctrine2.php +++ b/src/PEAR2/Mail/Queue/Container/doctrine2.php @@ -1,13 +1,349 @@ | + * +----------------------------------------------------------------------+ + * + * PHP Version 5.3 + * + * @category Mail + * @package Mail_Queue + * @author Leander Damme + * @license http://www.opensource.org/licenses/bsd-license.php The BSD License + * @link http://pear.php.net/package/Mail_Queue + */ + namespace PEAR2\Mail\Queue\Container; -use MDB2 as PearMDB2; use PEAR2\Mail\Queue\Container; use PEAR2\Mail\Queue\Exception; use PEAR2\Mail\Queue; use PEAR2\Mail\Queue\Body; +use PEAR2\Mail\Queue\Entity\Mail; +use PEAR2\Mail\Queue\Entity\Repository\MailRepository; + +/** + * Storage driver for fetching mail queue data with Doctrine2 + * + * This storage driver can use all databases which are supported + * by the DoctrineORM abstraction layer. + * + * @category Mail + * @package Mail_Queue + * @author Leander Damme + * @license http://www.opensource.org/licenses/bsd-license.php The BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_Queue + */ -class doctrin2 extends Container +class Doctrine2 extends Container { + var $errorMsg = 'doctrine failed: "%s", %s'; + + /** + * @var Doctrine\ORM\EntityManager + */ + protected $em; + + /** + * @var Mail Mail + */ + protected $entity; + + /** + * @var MailRepository MailRepository + */ + protected $repo; + + /** + * @var + */ + protected $config; + + /** + * Constructor + * + * Mail_Queue_Container_doctrine2() + * + * @param array $options An associative array of connection option. + * + */ + public function __construct(array $options) + { + + if (!isset($options['doctrine_em'])) { + throw new Exception( + 'No doctrine entity manager specified!', + Queue::ERROR_NO_OPTIONS + ); + } + $this->em = $options['doctrine_em']; + + $this->setOption(); + $this->repo = $this->em->getRepository('PEAR2\Mail\Queue\Entity\Mail'); + } + + /** + * Get the Doctrine EntityManager + * + * @return \Doctrine\ORM\EntityManager + */ + public function getEntityManager() + { + if (isset($this->em)) { + return $this->em; + } + $this->init(); + return $this->em; + } + + /** + * Get the Doctrine EntityManager + * + * @param Doctrine\ORM\EntityManager $em EntityManager + * + * @return \PEAR2\Mail\Queue\Container\doctrine2 + */ + public function setEntityManager($em) + { + $this->em = $em; + return $this; + } + + /** + * Preload mail to queue. + * + * @return true + * @throws Exception + */ + protected function _preload() + { + $queueCollection = $this->repo->preload($this->limit, $this->offset,$this->try); + + $this->_last_item = 0; + $this->queue_data = array(); //reset buffer + + foreach($queueCollection as $mail){ + $delete_after_send = (bool) $mail->__get('deleteAfterSend'); + + $this->queue_data[$this->_last_item] = new Body( + $mail->getId(), + $mail->getCreateTime(), + $mail->getTimeToSend(), + $mail->getSentTime(), + $mail->getIdUser(), + $mail->getIp(), + $mail->getSender(), + $this->_isSerialized($mail->getRecipient()) ? unserialize($mail->getRecipient()) : $mail->getRecipient(), + unserialize($mail->getHeaders()), + unserialize($mail->getBody()), + $delete_after_send, + $mail->getTrySent() + ); + $this->_last_item++; + } + + return true; + } + + /** + * Put new mail in queue. + * + * Mail_Queue_Container::put() + * + * @param string $time_to_send When mail have to be send + * @param integer $id_user Sender id + * @param string $ip Sender ip + * @param string $from Sender e-mail + * @param string $to Reciepient e-mail + * @param string $hdrs Mail headers (in RFC) + * @param string $body Mail body (in RFC) + * @param bool $delete_after_send (not sure wether in RFC) + * + * @return bool True on success + **/ + public function put($time_to_send, $id_user, $ip, $from, $to, $hdrs, $body, $delete_after_send) + { + $queueRecord = new Mail(); + + $queueRecord + ->__set('createTime', new \DateTime()) + ->__set('timeToSend', new \DateTime($time_to_send)) + ->__set('idUser', $id_user) + ->__set('ip', $ip) + ->__set('sender', $from) + ->__set('recipient', $to) + ->__set('headers', $hdrs) + ->__set('body', $body) + ->__set('deleteAfterSend', ($delete_after_send ? 1 : 0)); + + $this->em->persist($queueRecord); + $this->em->flush(); + + return $queueRecord->__get('id'); + } + + /** + * Check how many times mail was sent. + * + * @param Body $mail object + * + * @return mixed Integer or false if error. + */ + public function countSend(Body $mail) + { + $count = $mail->_try(); + + $mailRecord = $this->repo->find($mail->getId()); + + if (null == $mailRecord) { + throw new Exception( + sprintf($this->errorMsg, $mail->getId(), 'no message with id'), + Queue::ERROR_QUERY_FAILED + ); + } + + $mailRecord->__set('trySent',$count); + $this->em->persist($mailRecord); + $this->em->flush(); + + + return $count; + + // TODO: Implement countSend() method. + } + + /** + * Set mail as already sent. + * + * @param Body $mail object + * + * @return bool + */ + public function setAsSent(Body $mail) + { + $mailRecord = $this->repo->find($mail->getId()); + + if (null == $mailRecord) { + throw new Exception( + sprintf($this->errorMsg, $mail->getId(), 'no message with id'), + Queue::ERROR_QUERY_FAILED + ); + } + + $now = new \DateTime(); + $mailRecord->__set('sentTime',$now); + + $this->em->persist($mailRecord); + $this->em->flush(); + + return true; + + } + + /** + * Return mail by id $id (bypass mail_queue) + * + * @param integer $id Mail ID + * + * @return mixed Mail object or false on error. + */ + public function getMailById($id) + { + $mailRecord = $this->repo->find($id); + + if (null == $mailRecord) { + throw new Exception( + sprintf($this->errorMsg, $id, 'no message with id'), + Queue::ERROR_QUERY_FAILED + ); + } + + return new Body( + $mailRecord->getId(), + $mailRecord->getCreateTime(), + $mailRecord->getTimeToSend(), + $mailRecord->getTimeToSend(), + $mailRecord->getIdUser(), + $mailRecord->getIp(), + $mailRecord->getSender(), + $this->_isSerialized($mailRecord->getRecipient()) ? unserialize($mailRecord->getRecipient()) : $mailRecord->getRecipient(), + unserialize($mailRecord->getHeaders()), + unserialize($mailRecord->getBody()), + $mailRecord->getDeleteAfterSend(), + $mailRecord->getTrySent() + ); + } + + /** + * Return the number of emails currently in the queue. + * + * @return int + * @throws Exception + */ + public function getQueueCount() + { + $count = $this->repo->getQueueCount(); + return (int) $count; + + // TODO: Implement getQueueCount() method. + } + + /** + * Remove from queue mail with $id identifier. + * + * @param integer $id Mail ID + * + * @return bool True on success ale false. + */ + public function deleteMail($id) + { + $mailRecord = $this->repo->find($id); + + if (null == $mailRecord) { + throw new Exception( + sprintf($this->errorMsg, $id, 'no message with id'), + Queue::ERROR_QUERY_FAILED + ); + } + + $this->em->remove($mailRecord); + $this->em->flush(); + + return true; + } } diff --git a/src/PEAR2/Mail/Queue/Entity/Mail.php b/src/PEAR2/Mail/Queue/Entity/Mail.php new file mode 100644 index 0000000..d165e6c --- /dev/null +++ b/src/PEAR2/Mail/Queue/Entity/Mail.php @@ -0,0 +1,249 @@ +trySent = 0; + } + + /** + * Magic getter to expose protected properties. + * + * @param string $property '' + * + * @return mixed + */ + public function __get($property) + { + return $this->$property; + } + + /** + * Magic setter to save protected properties. + * + * @param string $property '' + * @param mixed $value '' + * + * @return void + */ + public function __set($property, $value) + { + $this->$property = $value; + return $this; + } + + /** + * Convert the object to an array. + * + * @return array + */ + public function getArrayCopy() + { + return get_object_vars($this); + } + + + /** + * get time queue item was created + * + * @return \DateTime + */ + public function getCreateTime() + { + return $this->createTime; + } + + /** + * get ID + * + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * get User ID + * + * @return mixed + */ + public function getIdUser() + { + return $this->idUser; + } + + /** + * get IP + * + * @return mixed + */ + public function getIp() + { + return $this->ip; + } + + /** + * get recipient + * + * @return mixed + */ + public function getRecipient() + { + return $this->recipient; + } + + /** + * get sender + * + * @return mixed + */ + public function getSender() + { + return $this->sender; + } + + /** + * get time mail is scheduled for + * + * @return \DateTime + */ + public function getTimeToSend() + { + return $this->timeToSend; + } + + /** + * get time mail was sent + * + * @return \DateTime + */ + public function getSentTime() + { + return $this->sentTime; + } + + + /** + * get body + * + * @return mixed + */ + public function getBody() + { + return $this->body; + } + + /** + * get delete after send + * + * @return bool + */ + public function getDeleteAfterSend() + { + return (boolean)$this->deleteAfterSend; + } + + /** + * get headers + * + * @return mixed + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * get number of tries sending + * + * @return mixed + */ + public function getTrySent() + { + return $this->trySent; + } +} diff --git a/src/PEAR2/Mail/Queue/Entity/Repository/MailRepository.php b/src/PEAR2/Mail/Queue/Entity/Repository/MailRepository.php new file mode 100644 index 0000000..29426c5 --- /dev/null +++ b/src/PEAR2/Mail/Queue/Entity/Repository/MailRepository.php @@ -0,0 +1,47 @@ +_em->createQuery( + 'SELECT m FROM PEAR2\Mail\Queue\Entity\Mail m' + . ' WHERE' + . ' m.sentTime is NULL AND' + . ' m.trySent < :try AND' + + . ' m.timeToSend <= :timeToSend' + . ' ORDER BY m.timeToSend' + ); + + if($limit){ + $query->setMaxResults($limit); + } + + if($offset){ + $query->setFirstResult($offset); + } + + $query + ->setParameter('timeToSend', new \DateTime()) + ->setParameter('try', $try); + + return $query->getResult(); + + } + + public function getQueueCount() + { + + $query = $this->_em->createQuery('SELECT COUNT(m.id) FROM PEAR2\Mail\Queue\Entity\Mail m'); + $count = $query->getSingleScalarResult(); + + return $count; + } +} diff --git a/tests/Mail/Queue/DoctrineContainerTest.php b/tests/Mail/Queue/DoctrineContainerTest.php index 302b2a0..4faa725 100644 --- a/tests/Mail/Queue/DoctrineContainerTest.php +++ b/tests/Mail/Queue/DoctrineContainerTest.php @@ -1,37 +1,203 @@ 1); + $body = 'Lorem ipsum'; -class DoctrineContainerTest extends \PHPUnit_Framework_TestCase -{ - protected $dbal; + $mailId = $this->queue->put($sender, $recipient, $headers, $body, 0, true, $id_user); + if (!is_numeric($mailId)) { + $this->fail("Could not save email."); + return; + } - public function setUp() + $message = $this->queue->container->getMailById($mailId); + $this->assertTrue(($message instanceof PEAR2\Mail\Queue\Body)); + + $this->assertEquals($mailId, $message->getId()); + $this->assertEquals($id_user, $message->getIdUser()); + $this->assertEquals('', $message->getIp()); + $this->assertEquals($sender, $message->getSender()); + $this->assertEquals($recipient, $message->getRecipient()); + $this->assertEquals($headers, $message->getHeaders()); + $this->assertEquals($body, $message->getBody()); + $this->assertTrue($message->isDeleteAfterSend()); + } + + + public function testGetMailById(){ + + $id_user = 1; + $sender = 'testsuite@example.org'; + $recipient = 'testcase@example.org'; + $headers = array('X-TestSuite' => 1); + $body = 'Lorem ipsum'; + + $mailId = $this->queue->put($sender, $recipient, $headers, $body, 0, true, $id_user); + if (!is_numeric($mailId)) { + $this->fail("Could not save email."); + return; + } + $message = $this->queue->sendMailById($mailId); + $this->assertEquals($message, true); + } + + /** + * @expectedException Exception + */ + public function testDeleteMailById(){ + + $id_user = 1; + $sender = 'testsuite@example.org'; + $recipient = 'testcase@example.org'; + $headers = array('X-TestSuite' => 1); + $body = 'Lorem ipsum'; + + $mailId = $this->queue->put($sender, $recipient, $headers, $body, 0, true, $id_user); + if (!is_numeric($mailId)) { + $this->fail("Could not save email."); + return; + } + $delResult = $this->getContainer()->deleteMail($mailId); + + $this->assertEquals($delResult, true); + + $findResult = $delResult = $this->getContainer()->getMailById($mailId); + + $this->assertEquals($findResult, false); + + + } + + public function testPut() { - $classLoader = new ClassLoader('Doctrine', '/Users/till/Documents/pear/share/pear'); - $classLoader->register(); + $id_user = 1; + $ip = '127.0.0.1'; + $sender = 'testsuite@example.org'; + $recipient = 'testcase@example.org'; + $headers = array('X-TestSuite' => 1); + $body = 'Lorem ipsum'; - $params = array( - 'dbname' => ':MEMORY:', - 'driver' => 'pdo_sqlite', - ); + $mailId = $this->queue->put($sender, $recipient, $headers, $body, 0, true, $id_user); + if (!is_numeric($mailId)) { + $this->handlePearError($mailId, "Could not save email."); + } - $this->dbal = DriverManager::getConnection($params, new Configuration); + $this->assertEquals(1, $mailId); // it's the first email, after all :-) + $this->assertEquals(1, count($this->queue->getQueueCount())); } - public function testInit() + /** + * Arrays with from, to, headers, body and when to send (= now). + * + * @return void + */ + public function testLimit() { - $container_opts = array( - 'type' => 'doctrine', - 'dsn' => $this->dbal, - 'mail_table' => 'mail_queue', + $emails = array( + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), + array('from@example.org', 'to@example.org', array('X-Test' => 'yes'), 'Hello World', 0), ); + + foreach ($emails as $email) { + $id = call_user_func_array(array($this->queue, 'put'), $email); + $this->assertInternalType('int', $id); + } + $this->assertEquals(count($emails), $this->queue->getQueueCount()); + + $this->queue->setBufferSize(2); + $this->assertTrue($this->queue->sendMailsInQueue(12)); + + $this->assertEquals(3, $this->queue->getQueueCount()); + + $this->assertFalse($this->queue->hasErrors()); + } + + /** + * This should return a MDB2_Error + * + * @expectedException PEAR2\Mail\Queue\Exception + */ + public function testSendMailByIdWithInvalidId() + { + $randomId = rand(1, 12); + $status = $this->queue->sendMailById($randomId); + } + + /** + * Queue two emails - to be send right away. + * + * @return void + */ + public function testSendMailsInQueue() + { + $id_user = 1; + $sender = 'testsuite@example.org'; + $recipient = 'testcase@example.org'; + $headers = array('X-TestSuite' => 1); + $body = 'Lorem ipsum'; + + $mailId1 = $this->queue->put($sender, $recipient, $headers, $body); + if ($mailId1 instanceof PEAR_Error) { + $this->fail("Queueing first mail failed: {$mailId1->getMessage()}"); + return; + } + + $id_user = 1; + $sender = 'testsuite@example.org'; + $recipient = 'testcase@example.org'; + $headers = array('X-TestSuite' => 2); + $body = 'Lorem ipsum sit dolor'; + + $mailId2 = $this->queue->put($sender, $recipient, $headers, $body); + if ($mailId2 instanceof PEAR_Error) { + $this->fail("Queueing first mail failed: {$mailId2->getMessage()}"); + return; + } + + $queueCount = $this->queue->getQueueCount(); + if ($this->queue->hasErrors()) { + $fail = ''; + foreach ($this->queue->getErrors() as $error) { + $fail .= $error->getMessage() . ", "; + } + $this->fail("Errors from getQueueCount: {$fail}"); + return; + } + $this->assertEquals(2, $queueCount, "Failed to count 2 messages."); + + $status = $this->queue->sendMailsInQueue(); + if ($status instanceof PEAR_Error) { + $this->fail("Error sending emails: {$status->getMessage()}."); + return; + } + + $this->assertTrue($status); + $this->assertEquals(0, $this->queue->getQueueCount(), "Mails did not get removed?"); } } diff --git a/tests/Mail/QueueDoctrineAbstract.php b/tests/Mail/QueueDoctrineAbstract.php new file mode 100644 index 0000000..31cfc8e --- /dev/null +++ b/tests/Mail/QueueDoctrineAbstract.php @@ -0,0 +1,133 @@ +newDefaultAnnotationDriver(); + + AnnotationReader::addGlobalIgnoredName('package_version'); + $annotationReader = new AnnotationReader; + $cachedAnnotationReader = new \Doctrine\Common\Annotations\CachedReader( + $annotationReader, // use reader + $cache // and a cache driver + ); + + $annotationDriver = new \Doctrine\ORM\Mapping\Driver\AnnotationDriver( + $cachedAnnotationReader//, // our cached annotation reader + ); + + $config->setMetadataDriverImpl($annotationDriver); + $config->setMetadataCacheImpl($cache); + $config->setQueryCacheImpl($cache); + $config->setProxyDir(__DIR__ . '/../Proxy'); + $config->setProxyNamespace('testProxy'); + $config->setAutoGenerateProxyClasses(1); + + $connectionConfig = array( + 'driver' => 'pdo_sqlite', + 'dbname' => '', + 'user' => '', + 'host' => '', + 'password' => '', + 'memory' => true + + ); + + $this->em = EntityManager::create( + $connectionConfig, + $config + ); + + $doctrineDriver = new PEAR2\Mail\Queue\Container\doctrine2(array('doctrine_em' => $this->em)); + $this->container = $doctrineDriver; + + $this->em = $doctrineDriver->getEntityManager(); + + $this->initDoctrineTestSetup(); + + + $container_opts = array( + 'type' => 'doctrine2', + 'doctrine_em' => $this->em + ); + + /** + * @see Mail_mock + */ + $mail_opts = array('driver' => 'mock'); + + $this->queue = new Queue($container_opts, $mail_opts); + if ($this->queue->hasErrors()) { + $errors = $this->queue->getErrors(); + $fail = "The following errors occurred:\n"; + foreach ($errors as $error) { + $fail .= $error->getMessage() . "\n"; + } + $this->fail($fail); + } + } + + + protected function initDoctrineTestSetup() + { + + $tool = new \Doctrine\ORM\Tools\SchemaTool($this->em); + $classes = array( + $this->em->getClassMetadata('PEAR2\Mail\Queue\Entity\Mail'), + ); + + $tool->createSchema($classes); + } + + /** + * Remove the table/artifacts. + * + * @return void + * @see self::setUpDatabase() + */ + public function tearDown() + { + $tool = new \Doctrine\ORM\Tools\SchemaTool($this->em); + $tool->dropDatabase(); + unset($this->em); + parent::tearDown(); + + + //$this->queue->container->db->disconnect(); + unset($this->queue); + } + + public function getContainer(){ + return $this->container; + } +} diff --git a/tests/TestInit.php b/tests/TestInit.php index f5bc3c4..2283c02 100644 --- a/tests/TestInit.php +++ b/tests/TestInit.php @@ -7,6 +7,7 @@ exit(1); } +require_once __DIR__ . '/../vendor/autoload.php'; require_once 'MDB2.php'; class MailQueueTestInit