From 492f35621c213535c38156cfd74952cc58c51791 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 22 Oct 2013 17:22:08 +0300 Subject: [PATCH] Email components hierarchy created. SwiftMailer applied as email solution. --- .travis.yml | 2 +- framework/yii/email/BaseMailer.php | 125 ++++++++++++++++ framework/yii/email/BaseMessage.php | 125 ++++++++++++++++ framework/yii/email/Message.php | 34 +++++ framework/yii/email/swift/Mailer.php | 180 +++++++++++++++++++++++ framework/yii/email/swift/Message.php | 116 +++++++++++++++ framework/yii/email/swift/autoload.php | 9 ++ tests/unit/framework/email/BaseMailerTest.php | 97 ++++++++++++ tests/unit/framework/email/swift/MailerTest.php | 90 ++++++++++++ tests/unit/framework/email/swift/MessageTest.php | 40 +++++ 10 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 framework/yii/email/BaseMailer.php create mode 100644 framework/yii/email/BaseMessage.php create mode 100644 framework/yii/email/Message.php create mode 100644 framework/yii/email/swift/Mailer.php create mode 100644 framework/yii/email/swift/Message.php create mode 100644 framework/yii/email/swift/autoload.php create mode 100644 tests/unit/framework/email/BaseMailerTest.php create mode 100644 tests/unit/framework/email/swift/MailerTest.php create mode 100644 tests/unit/framework/email/swift/MessageTest.php diff --git a/.travis.yml b/.travis.yml index a905b36..0218654 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ before_script: - tests/unit/data/travis/cubrid-setup.sh script: - - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata + - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,swiftmailer after_script: - php vendor/bin/coveralls diff --git a/framework/yii/email/BaseMailer.php b/framework/yii/email/BaseMailer.php new file mode 100644 index 0000000..92fa3df --- /dev/null +++ b/framework/yii/email/BaseMailer.php @@ -0,0 +1,125 @@ + + * @since 2.0 + */ +abstract class BaseMailer extends Component +{ + /** + * @var \yii\base\View|array view instance or its array configuration. + */ + private $_view = []; + /** + * @var array configuration, which should be applied by default to any new created + * email message instance. + * For example: + * ~~~ + * array( + * 'encoding' => 'UTF-8', + * 'from' => 'noreply@mydomain.com', + * 'bcc' => 'email.test@mydomain.com', + * ) + * ~~~ + */ + private $_defaultMessageConfig = []; + + /** + * @param array|\yii\base\View $view view instance or its array configuration. + * @throws \yii\base\InvalidConfigException on invalid argument. + */ + public function setView($view) + { + if (!is_array($view) && !is_object($view)) { + throw new InvalidConfigException('"' . get_class($this) . '::view" should be either object or array, "' . gettype($view) . '" given.'); + } + $this->_view = $view; + } + + /** + * @return \yii\base\View view instance. + */ + public function getView() + { + if (!is_object($this->_view)) { + $this->_view = $this->createView($this->_view); + } + return $this->_view; + } + + /** + * @param array $defaultMessageConfig default message config + */ + public function setDefaultMessageConfig(array $defaultMessageConfig) + { + $this->_defaultMessageConfig = $defaultMessageConfig; + } + + /** + * @return array default message config + */ + public function getDefaultMessageConfig() + { + return $this->_defaultMessageConfig; + } + + /** + * Creates view instance from given configuration. + * @param array $config view configuration. + * @return \yii\base\View view instance. + */ + protected function createView(array $config) + { + if (!array_key_exists('class', $config)) { + $config['class'] = '\yii\base\View'; + } + $config['context'] = $this; + return Yii::createObject($config); + } + + /** + * Sends the given email message. + * @param object $message email message instance + * @return boolean whether the message has been sent. + */ + abstract public function send($message); + + /** + * Sends a couple of messages at once. + * Note: some particular mailers may benefit from sending messages as batch, + * saving resources, for example on open/close connection operations, + * they may override this method to create their specific implementation. + * @param array $messages list of email messages, which should be sent. + * @return integer number of successfull sends + */ + public function sendMultiple(array $messages) { + $successCount = 0; + foreach ($messages as $message) { + if ($this->send($message)) { + $successCount++; + } + } + return $successCount; + } +} \ No newline at end of file diff --git a/framework/yii/email/BaseMessage.php b/framework/yii/email/BaseMessage.php new file mode 100644 index 0000000..8127c9c --- /dev/null +++ b/framework/yii/email/BaseMessage.php @@ -0,0 +1,125 @@ + + * @since 2.0 + */ +abstract class BaseMessage extends Object +{ + /** + * @return \yii\email\BaseMailer + */ + public function getMailer() + { + return Yii::$app->getComponent('email'); + } + + /** + * Initializes the object. + * This method is invoked at the end of the constructor after the object is initialized with the + * given configuration. + */ + public function init() + { + Yii::configure($this, $this->getMailer()->getDefaultMessageConfig()); + } + + /** + * Sends this email message. + * @return boolean success. + */ + public function send() + { + return $this->getMailer()->send($this); + } + + /** + * Sets message sender. + * @param string|array $from sender email address, if array is given, + * its first element should be sender email address, second - sender name. + */ + abstract public function setFrom($from); + + /** + * Sets message receiver. + * @param string|array $to receiver email address, if array is given, + * its first element should be receiver email address, second - receiver name. + */ + abstract public function setTo($to); + + /** + * Sets message subject. + * @param string $subject message subject + */ + abstract public function setSubject($subject); + + /** + * Sets message plain text content. + * @param string $text message plain text content. + */ + abstract public function setText($text); + + /** + * Sets message HTML content. + * @param string $html message HTML content. + */ + abstract public function setHtml($html); + + /** + * Create file attachment for the email message. + * @param string $content attachment file content. + * @param string $fileName attachment file name. + * @param string $contentType MIME type of the attachment file, by default 'application/octet-stream' will be used. + */ + abstract public function createAttachment($content, $fileName, $contentType = 'application/octet-stream'); + + /** + * Attaches existing file to the email message. + * @param string $fileName full file name + * @param string $contentType MIME type of the attachment file, if empty it will be suggested automatically. + * @param string $attachFileName name, which should be used for attachment, if empty file base name will be used. + * @throws \yii\base\InvalidParamException if given file does not exist. + */ + public function attachFile($fileName, $contentType = null, $attachFileName = null) + { + if (!file_exists($fileName)) { + throw new InvalidParamException('Unable to attach file "' . $fileName . '": file does not exists!'); + } + if (empty($contentType)) { + $contentType = FileHelper::getMimeType($fileName); + } + if (empty($attachFileName)) { + $attachFileName = basename($fileName); + } + $content = file_get_contents($fileName); + $this->createAttachment($content, $attachFileName, $contentType); + } +} \ No newline at end of file diff --git a/framework/yii/email/Message.php b/framework/yii/email/Message.php new file mode 100644 index 0000000..c7ec534 --- /dev/null +++ b/framework/yii/email/Message.php @@ -0,0 +1,34 @@ +from = 'sender@domain.com'; + * $email->to = 'receiver@domain.com'; + * $email->subject = 'Message Subject'; + * $email->text = 'Message Content'; + * $email->send(); + * ~~~ + * + * This particular class uses 'SwiftMailer' library to perform the message sending. + * Note: you can replace usage of this class by your own one, using [[Yii::$classMap]]: + * ~~~ + * Yii::$classMap['yii\email\Message'] = '/path/to/my/email/Message.php' + * ~~~ + * + * @author Paul Klimov + * @since 2.0 + */ +class Message extends SwiftMessage {} \ No newline at end of file diff --git a/framework/yii/email/swift/Mailer.php b/framework/yii/email/swift/Mailer.php new file mode 100644 index 0000000..1f589a2 --- /dev/null +++ b/framework/yii/email/swift/Mailer.php @@ -0,0 +1,180 @@ + array( + * ... + * 'email' => array( + * 'class' => 'yii\email\swift\Mailer', + * 'transport' => [ + * 'class' => 'Swift_SmtpTransport', + * 'host' => 'localhost', + * 'username' => 'username', + * 'password' => 'password', + * 'port' => '587', + * 'encryption' => 'tls', + * ], + * ), + * ... + * ), + * ~~~ + * + * @see http://swiftmailer.org + * + * @author Paul Klimov + * @since 2.0 + */ +class Mailer extends BaseMailer +{ + /** + * @var string|callback SwiftMailer autoloader callback or path to autoloader file. + * If the SwiftMailer classes autoloading is already managed in some other place, + * for example via Composer, you should leave this field blank. + */ + public $autoload; + /** + * @var \Swift_Mailer Swift mailer instance. + */ + private $_swiftMailer; + /** + * @var \Swift_Transport|array Swift transport instance or its array configuration. + */ + private $_transport = []; + + /** + * @return array|\Swift_Mailer Swift mailer instance or array configuration. + */ + public function getSwiftMailer() + { + if (!is_object($this->_swiftMailer)) { + $this->_swiftMailer = $this->createSwiftMailer(); + } + return $this->_swiftMailer; + } + + /** + * @param array|\Swift_Transport $transport + * @throws \yii\base\InvalidConfigException on invalid argument. + */ + public function setTransport($transport) + { + if (!is_array($transport) && !is_object($transport)) { + throw new InvalidConfigException('"' . get_class($this) . '::transport" should be either object or array, "' . gettype($transport) . '" given.'); + } + $this->_transport = $transport; + } + + /** + * @return array|\Swift_Transport + */ + public function getTransport() + { + if (!is_object($this->_transport)) { + $this->_transport = $this->createTransport($this->_transport); + } + return $this->_transport; + } + + /** + * @inheritdoc + */ + public function init() + { + $this->setupSwiftMailerAutoload(); + } + + /** + * Sets up the SwiftMailer autoloader, if it is specified. + */ + protected function setupSwiftMailerAutoload() + { + if (!class_exists('Swift', false)) { + if (empty($this->autoload)) { + $this->autoload = __DIR__ . '/autoload.php'; + } + if (is_string($this->autoload)) { + if (file_exists($this->autoload)) { + require_once($this->autoload); + } elseif (function_exists($this->autoload)) { + spl_autoload_register($this->autoload); + } else { + throw new InvalidConfigException('"' . get_class($this) . '::autoload" value "' . $this->autoload . '" is invalid: no such function or file exists.'); + } + } else { + spl_autoload_register($this->autoload); + } + } + } + + /** + * @inheritdoc + */ + public function send($message) + { + return ($this->getSwiftMailer()->send($message->getSwiftMessage()) > 0); + } + + /** + * Creates Swift mailer instance. + * @return \Swift_Mailer mailer instance. + */ + protected function createSwiftMailer() + { + return \Swift_Mailer::newInstance($this->getTransport()); + } + + /** + * Creates email transport instance by its array configuration. + * @param array $config transport configuration. + * @throws \yii\base\InvalidConfigException on invalid transport configuration. + * @return \Swift_Transport transport instance. + */ + protected function createTransport(array $config) + { + if (array_key_exists('class', $config)) { + $className = $config['class']; + unset($config['class']); + } else { + $className = 'Swift_MailTransport'; + } + $transport = call_user_func([$className, 'newInstance']); + if (!empty($config)) { + foreach ($config as $name => $value) { + if (property_exists($transport, $name)) { + $transport->$name = $value; + } else { + $setter = 'set' . $name; + if (method_exists($transport, $setter)) { + $transport->$setter($value); + } else { + throw new InvalidConfigException('Setting unknown property: ' . get_class($transport) . '::' . $name); + } + } + } + } + return $transport; + } + + /** + * Creates the Swift email message instance. + * @return \Swift_Message email message instance. + */ + public function createSwiftMessage() + { + return new \Swift_Message(); + } +} \ No newline at end of file diff --git a/framework/yii/email/swift/Message.php b/framework/yii/email/swift/Message.php new file mode 100644 index 0000000..49a4b75 --- /dev/null +++ b/framework/yii/email/swift/Message.php @@ -0,0 +1,116 @@ + + * @since 2.0 + */ +class Message extends BaseMessage +{ + /** + * @var \Swift_Message Swift message instance. + */ + private $_swiftMessage; + + /** + * @return \Swift_Message Swift message instance. + */ + public function getSwiftMessage() + { + if (!is_object($this->_swiftMessage)) { + $this->_swiftMessage = $this->getMailer()->createSwiftMessage(); + } + return $this->_swiftMessage; + } + + /** + * Sets message sender. + * @param string|array $from sender email address, if array is given, + * its first element should be sender email address, second - sender name. + */ + public function setFrom($from) + { + if (is_array($from)) { + list ($address, $name) = $from; + } else { + $address = $from; + $name = null; + } + $this->getSwiftMessage()->setFrom($address, $name); + $this->getSwiftMessage()->setReplyTo($address, $name); + } + + /** + * Sets message receiver. + * @param string|array $to receiver email address, if array is given, + * its first element should be receiver email address, second - receiver name. + */ + public function setTo($to) + { + if (is_array($to)) { + list ($address, $name) = $to; + } else { + $address = $to; + $name = null; + } + $this->getSwiftMessage()->setTo($address, $name); + } + + /** + * Sets message subject. + * @param string $subject message subject + */ + public function setSubject($subject) + { + $this->getSwiftMessage()->setSubject($subject); + } + + /** + * Sets message plain text content. + * @param string $text message plain text content. + */ + public function setText($text) + { + $this->getSwiftMessage()->setBody($text, 'text/plain'); + } + + /** + * Sets message HTML content. + * @param string $html message HTML content. + */ + public function setHtml($html) + { + $this->getSwiftMessage()->setBody($html, 'text/html'); + } + + /** + * Create file attachment for the email message. + * @param string $content - attachment file content. + * @param string $fileName - attachment file name. + * @param string $contentType - MIME type of the attachment file, by default 'application/octet-stream' will be used. + */ + public function createAttachment($content, $fileName, $contentType = 'application/octet-stream') + { + if (empty($contentType)) { + $contentType = 'application/octet-stream'; + } + $attachment = \Swift_Attachment::newInstance($content, $fileName, $contentType); + $this->getSwiftMessage()->attach($attachment); + } +} \ No newline at end of file diff --git a/framework/yii/email/swift/autoload.php b/framework/yii/email/swift/autoload.php new file mode 100644 index 0000000..494150f --- /dev/null +++ b/framework/yii/email/swift/autoload.php @@ -0,0 +1,9 @@ +mockApplication(); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + return $component; + } + + // Tests : + + public function testSetupView() + { + $mailer = new Mailer(); + + $view = new View(); + $mailer->setView($view); + $this->assertEquals($view, $mailer->getView(), 'Unable to setup view!'); + } + + public function testGetDefaultView() + { + $mailer = new Mailer(); + $view = $mailer->getView(); + $this->assertTrue(is_object($view), 'Unable to get default view!'); + } + + public function testDefaultMessageConfig() + { + $defaultMessageConfig = array( + 'id' => 'test-id', + 'encoding' => 'test-encoding', + ); + Yii::$app->getComponent('email')->setDefaultMessageConfig($defaultMessageConfig); + + $message = new Message(); + + foreach ($defaultMessageConfig as $name => $value) { + $this->assertEquals($value, $message->$name); + } + } +} + +/** + * Test Mailer class + */ +class Mailer extends BaseMailer +{ + public $sentMessages = array(); + + public function send($message) + { + $this->sentMessages[] = $message; + } +} + +/** + * Test Message class + */ +class Message extends BaseMessage +{ + public $id; + public $encoding; + + public function setFrom($from) {} + + public function setTo($to) {} + + public function setSubject($subject) {} + + public function setText($text) {} + + public function setHtml($html) {} + + public function createAttachment($content, $fileName, $contentType = 'application/octet-stream') {} +} \ No newline at end of file diff --git a/tests/unit/framework/email/swift/MailerTest.php b/tests/unit/framework/email/swift/MailerTest.php new file mode 100644 index 0000000..f984139 --- /dev/null +++ b/tests/unit/framework/email/swift/MailerTest.php @@ -0,0 +1,90 @@ +mockApplication(array( + 'vendorPath' => Yii::getAlias('@yiiunit/vendor') + )); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + return $component; + } + + // Tests : + + public function testSetupTransport() + { + $mailer = new Mailer(); + + $transport = \Swift_MailTransport::newInstance(); + $mailer->setTransport($transport); + $this->assertEquals($transport, $mailer->getTransport(), 'Unable to setup transport!'); + } + + /** + * @depends testSetupTransport + */ + public function testConfigureTransport() + { + $mailer = new Mailer(); + + $transportConfig = [ + 'class' => 'Swift_SmtpTransport', + 'host' => 'localhost', + ]; + $mailer->setTransport($transportConfig); + $transport = $mailer->getTransport(); + $this->assertTrue(is_object($transport), 'Unable to setup transport via config!'); + $this->assertEquals($transportConfig['class'], get_class($transport), 'Invalid transport class!'); + $this->assertEquals($transportConfig['host'], $transport->getHost(), 'Invalid transport host!'); + } + + public function testGetSwiftMailer() + { + $mailer = new Mailer(); + $this->assertTrue(is_object($mailer->getSwiftMailer()), 'Unable to get Swift mailer instance!'); + } + + public function testCreateSwiftMessage() + { + $mailer = new Mailer(); + $message = $mailer->createSwiftMessage(); + $this->assertTrue(is_object($message), 'Unable to create Swift message instance!'); + } + + /** + * @depends testGetSwiftMailer + * @depends testCreateSwiftMessage + */ + public function testSend() + { + $emailAddress = 'someuser@somedomain.com'; + $message = new Message(); + $message->setTo($emailAddress); + $message->setFrom($emailAddress); + $message->setSubject('Yii Swift Test'); + $message->setText('Yii Swift Test body'); + $message->send(); + $this->assertTrue(true); + } +} \ No newline at end of file diff --git a/tests/unit/framework/email/swift/MessageTest.php b/tests/unit/framework/email/swift/MessageTest.php new file mode 100644 index 0000000..50742c9 --- /dev/null +++ b/tests/unit/framework/email/swift/MessageTest.php @@ -0,0 +1,40 @@ +mockApplication(array( + 'vendorPath' => Yii::getAlias('@yiiunit/vendor') + )); + Yii::$app->setComponent('email', $this->createTestEmailComponent()); + } + + /** + * @return Mailer test email component instance. + */ + protected function createTestEmailComponent() + { + $component = new Mailer(); + return $component; + } + + // Tests : + + public function testGetSwiftMessage() + { + $message = new Message(); + $this->assertTrue(is_object($message->getSwiftMessage()), 'Unable to get Swift message!'); + } +} \ No newline at end of file