diff --git a/composer.json b/composer.json index 7aebf18..e40538a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name" : "ocubom/email-address", "description" : "Email address value object checked against RFC 3696, RFC 1123, RFC 4291, RFC 5321 and RFC 5322.", - "keywords" : [ "address", "email", "validator", "RFC 1123", "RFC 3696", "RFC 4291", "RFC 5321", "RFC 5322" ], + "keywords" : [ "address", "email", "validator", "RFC 1123", "RFC 3696", "RFC 4291", "RFC 5321", "RFC 5322", "RFC1123", "RFC3696", "RFC4291", "RFC5321", "RFC5322" ], "homepage" : "http://github.com/ocubom/email-address", "license" : "MIT", "authors" : [{ diff --git a/src/Address.php b/src/Address.php index 7724f14..4896b11 100644 --- a/src/Address.php +++ b/src/Address.php @@ -11,6 +11,8 @@ namespace Ocubom\Email; +use Ocubom\Email\Exception\InvalidEmailAddressException; + /** * Email address value * @@ -37,9 +39,9 @@ class Address */ public function __construct($address, $checkdns = true) { - $code = is_email($address, $checkdns, true, $parsed); - if (ISEMAIL_THRESHOLD < $code) { - throw new \RuntimeException(sprintf('Invalid email address "%s"', $address), $code); + $diagnosis = new Diagnosis(is_email($address, $checkdns, true, $parsed)); + if (ISEMAIL_THRESHOLD < $diagnosis->severity) { + throw new InvalidEmailAddressException($address, $diagnosis); } $this->data = array( @@ -53,7 +55,12 @@ public function __construct($address, $checkdns = true) // Diagnoses // http://github.com/dominicsayers/isemail/blob/master/is_email.php#L61 - 'status' => $parsed['status'], + 'status' => array_map( + function ($status) { + return new Diagnosis($status); + }, + $parsed['status'] + ), ); } diff --git a/src/Diagnosis.php b/src/Diagnosis.php new file mode 100644 index 0000000..864121c --- /dev/null +++ b/src/Diagnosis.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ocubom\Email; + +/** + * Diagnosis + * + * @author Oscar Cubo Medina + * + * @property-read string $description The detailed description + * @property-read string $category The category + * @property-read string $name Internal name of the diagnose + * @property-read integer $severity The severity level + */ +class Diagnosis extends Meta +{ + /** + * Translations + * + * @var array + */ + protected static $properties = array( + 'description' => 'description', + 'name' => 'id', + 'severity' => 'value', + ); + + /** + * Constructor. + * + * @param mixed $name Diagnosis name or identifier + */ + public function __construct($name) + { + if (!array_key_exists('category', static::$properties)) { + static::$properties['category'] = function ($diagnosis) { + return new DiagnosisCategory($diagnosis['category']); + }; + } + + parent::__construct($name, 'diagnoses'); + } +} diff --git a/src/DiagnosisCategory.php b/src/DiagnosisCategory.php new file mode 100644 index 0000000..c0cce2a --- /dev/null +++ b/src/DiagnosisCategory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ocubom\Email; + +/** + * Diagnosis categories + * + * @author Oscar Cubo Medina + * + * @property-read string $description The detailed description + * @property-read string $name Internal name of the diagnose + * @property-read integer $severity The severity level + */ +class DiagnosisCategory extends Meta +{ + /** + * Translations + * + * @var array + */ + protected static $properties = array( + 'description' => 'description', + 'name' => 'id', + 'severity' => 'value', + ); + + /** + * Constructor. + * + * @param mixed $name Diagnosis name or identifier + */ + public function __construct($name) + { + parent::__construct($name, 'categories'); + } +} diff --git a/src/Exception/InvalidEmailAddressException.php b/src/Exception/InvalidEmailAddressException.php new file mode 100644 index 0000000..cd5099d --- /dev/null +++ b/src/Exception/InvalidEmailAddressException.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ocubom\Email\Exception; + +use Ocubom\Email\Diagnosis; + +/** + * Invalid Email Address detected + * + * @author Oscar Cubo Medina + */ +class InvalidEmailAddressException extends \UnexpectedValueException +{ + /** + * Constructor. + * + * @param string $address The email address that fails its verification. + * @param Diagnosis $diagnosis The diagnosis + * @param Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($address, Diagnosis $diagnosis, \Exception $previous = null) + { + parent::__construct( + sprintf( + 'Invalid email address "%s": %s', + $address, + $diagnosis->description + ), + $diagnosis->severity, + $previous + ); + } +} diff --git a/src/Meta.php b/src/Meta.php new file mode 100644 index 0000000..38a2a28 --- /dev/null +++ b/src/Meta.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ocubom\Email; + +/** + * Diagnosis metadata dictionary + * + * @author Oscar Cubo Medina + * + * @property-read string $description The detailed description + * @property-read string $name Internal name of the diagnose + * @property-read integer $severity The severity level + */ +abstract class Meta +{ + /** + * Item identifier + * + * @var integer + */ + private $item; + + /** + * Database + * + * {@see http://github.com/dominicsayers/isemail/blob/master/test/meta.xml} + * + * @var array + */ + private static $items; + + /** + * Public properties + * + * @var array + */ + protected static $properties = array(); + + /** + * Constructor. + * + * @param mixed $name Metadata item name or identifier + * @param string $class Class of the data to load + */ + protected function __construct($name, $class) + { + if (!isset(self::$items[get_class($this)])) { + self::$items[get_class($this)] = require __DIR__ . '/../Resources/data/' . $class . '.php'; + } + + $this->item = defined($name) ? constant($name) : $name; + if (!array_key_exists($this->item, self::$items[get_class($this)])) { + throw new \InvalidArgumentException(sprintf('Unknown %s identifier "%s"', $class, $name)); + } + } + + /** + * Whether or not a property exists. + * + * @param string $name A property to check for. + * + * @return boolean True if the property exists + */ + public function __isset($name) + { + return null !== $this->getGetter($name); + } + + /** + * Returns the specified property value. + * + * @param string $name The property to retrieve. + * + * @return mixed + */ + public function __get($name) + { + // Return value + $getter = $this->getGetter($name); + if (null !== $getter) { + return $getter(); + } + + // Simulate PHP native behaviour + // http://php.net/manual/en/language.oop5.overloading.php#example-232 + trigger_error(sprintf('Undefined property: %s::$%s', get_class($this), $name)); + + // Previous sentence must abort execution on tests: this will never be called + // @codeCoverageIgnoreStart + return null; + // @codeCoverageIgnoreEnd + } + + /** + * Assigns a value to the specified property. + * + * @param string $name The property to assign the value to. + * @param mixed $value The value to set. + */ + final public function __set($name, $value) + { + // Avoid changes on properties + trigger_error(sprintf('Cannot modify read-only class property: %s::$%s', get_class($this), $name, $value)); + + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + /** + * Unsets a property. + * + * @param string $name The property to unset. + */ + final public function __unset($name) + { + // Avoid changes on properties + trigger_error(sprintf('Cannot delete read-only class property: %s::$%s', get_class($this), $name)); + + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + /** + * Provide a function to get the value of a property + * + * @param string $name The property name + * + * @return Callable or null if no getter is available + */ + private function getGetter($name) + { + // Obtain value (will be needed) + $val = self::$items[get_class($this)][$this->item]; + + // Check for property "translation" + $key = array_key_exists($name, static::$properties) ? static::$properties[$name] : $name; + + // Use custom callback function to convert/extract value + if (is_callable($key)) { + return (function () use ($key, $val) { + return $key($val); + }); + } + + // Return the value as-is + if (array_key_exists($key, $val)) { + return (function () use ($key, $val) { + return $val[$key]; + }); + } + + // The propertie does not exists + return null; + } + + /** + * String representation + * + * @return string + */ + public function __toString() + { + return self::$items[get_class($this)][$this->item]['id']; + } +} diff --git a/tests/AddressTest.php b/tests/AddressTest.php index c375c59..0ebcee7 100644 --- a/tests/AddressTest.php +++ b/tests/AddressTest.php @@ -12,6 +12,7 @@ namespace Ocubom\Email\Tests; use Ocubom\Email\Address as EmailAddress; +use Ocubom\Email\Diagnosis; /** * Email Address Tests @@ -35,7 +36,7 @@ public function testValidEmailAddress() // Must return parsed values $this->assertEquals('JohnDoe', $obj->local); $this->assertEquals('example.com', $obj->domain); - $this->assertEquals(array(ISEMAIL_DNSWARN_NO_MX_RECORD), $obj->status); + $this->assertEquals(array(new Diagnosis(ISEMAIL_DNSWARN_NO_MX_RECORD)), $obj->status); // Must convert into string $this->assertEquals('JohnDoe@example.com', (string) $obj, 'Email address must convert to string'); @@ -56,7 +57,7 @@ public function testValidEmailAddressWithoutDNS() // Must return parsed values $this->assertEquals('JohnDoe', $obj->local); $this->assertEquals('example.com', $obj->domain); - $this->assertEquals(array(ISEMAIL_VALID), $obj->status); + $this->assertEquals(array(new Diagnosis(ISEMAIL_VALID)), $obj->status); // Must convert into string $this->assertEquals('JohnDoe@example.com', (string) $obj, 'Email address must convert to string'); @@ -65,7 +66,7 @@ public function testValidEmailAddressWithoutDNS() /** * Invalid address must generate exceptions * - * @expectedException \RuntimeException + * @expectedException \Ocubom\Email\Exception\InvalidEmailAddressException * @expectedExceptionMessage Invalid email address "" * @expectedExceptionCode 131 */ diff --git a/tests/DiagnosisTest.php b/tests/DiagnosisTest.php new file mode 100644 index 0000000..e3f45c6 --- /dev/null +++ b/tests/DiagnosisTest.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ocubom\Email\Tests; + +use Ocubom\Email\Diagnosis; + +/** + * Test Email utility functions + * + * @author Oscar Cubo Medina + */ +class DiagnosisTest extends \PHPUnit_Framework_TestCase +{ + /** + * Valid address + * + * @param mixed $args Argument to create Diagnosis + * @param array $diagnosis Expected diagnosis data + * @param array $category Expected diagnosis category data + * + * @dataProvider provideDiagnoses + */ + public function testValid($args, $diagnosis, $category) + { + // Check diagnosis + $obj = new Diagnosis($args); + // Must have local, domain and status properties + $this->assertTrue(isset($obj->description), 'Email diagnosis must have description attribute'); + $this->assertTrue(isset($obj->category), 'Email diagnosis must have category attribute'); + $this->assertTrue(isset($obj->name), 'Email diagnosis must have name attribute'); + $this->assertTrue(isset($obj->severity), 'Email diagnosis must have severity attribute'); + // Must return parsed values + $this->assertEquals($diagnosis['description'], $obj->description); + $this->assertEquals($diagnosis['id'], $obj->name); + $this->assertEquals($diagnosis['value'], $obj->severity); + // Must convert into string + $this->assertEquals($diagnosis['id'], (string) $obj, 'Email diagnosis must convert to string'); + + // Check category + $obj = $obj->category; + // Must have local, domain and status properties + $this->assertTrue(isset($obj->description), 'Email diagnosis must have description attribute'); + $this->assertTrue(isset($obj->name), 'Email diagnosis must have name attribute'); + $this->assertTrue(isset($obj->severity), 'Email diagnosis must have severity attribute'); + // Must return parsed values + $this->assertEquals($category['description'], $obj->description); + $this->assertEquals($category['id'], $obj->name); + $this->assertEquals($category['value'], $obj->severity); + // Must convert into string + $this->assertEquals($category['id'], (string) $obj, 'Email diagnosis must convert to string'); + } + + /** + * Invalid address must generate exceptions + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unknown diagnoses identifier "-1" + * @expectedExceptionCode 0 + */ + public function testInvalid() + { + $obj = new Diagnosis(-1); + + echo $obj; + } + + /** + * Access to undefined properties must generate errors + * + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessage Undefined property: Ocubom\Email\Diagnosis::$noexists + * @expectedExceptionCode 1024 + */ + public function testUndefinedProperties() + { + $obj = new Diagnosis(ISEMAIL_VALID); + + echo $obj->noexists; + } + + /** + * Test that email Meta is inmutable + * + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessage Cannot modify read-only class property: Ocubom\Email\Diagnosis::$severity + * @expectedExceptionCode 1024 + */ + public function testInmutableHasSetDisabled() + { + $obj = new Diagnosis(ISEMAIL_VALID); + + $obj->severity = 0; + } + + /** + * Test that email address is inmutable + * + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessage Cannot delete read-only class property: Ocubom\Email\Diagnosis::$description + * @expectedExceptionCode 1024 + */ + public function testInmutableHasUnsetDisabled() + { + $obj = new Diagnosis(ISEMAIL_VALID); + + unset($obj->description); + } + + /** + * Provide diagnosis + * + * @return array + */ + public function provideDiagnoses() + { + $categories = require(__DIR__ . '/../Resources/data/categories.php'); + + return array_map( + function ($diagnose) use ($categories) { + return array( + 'id' => $diagnose['value'], + 'diagnosis' => $diagnose, + 'category' => $categories[$diagnose['category']], + ); + }, + require(__DIR__ . '/../Resources/data/diagnoses.php') + ); + } + + /** + * Returns reflected class to test + * + * @return \ReflectionClass + */ + protected function getClass() + { + // Cache instance on first call + static $class = null; + if (null === $class) { + $class = new \ReflectionClass('\\Xpl\\DateTime\\DateTimeInmutable'); + } + + return $class; + } +}