Skip to content

Commit

Permalink
Better diagnoses
Browse files Browse the repository at this point in the history
Use metadata to provide more information about email address problems
  • Loading branch information
ocubom committed Sep 16, 2014
1 parent a2cd96e commit 5512a22
Show file tree
Hide file tree
Showing 8 changed files with 485 additions and 8 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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" : [{
Expand Down
15 changes: 11 additions & 4 deletions src/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Ocubom\Email;

use Ocubom\Email\Exception\InvalidEmailAddressException;

/**
* Email address value
*
Expand All @@ -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(
Expand All @@ -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']
),
);
}

Expand Down
52 changes: 52 additions & 0 deletions src/Diagnosis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of "Email Address" component.
*
* © Oscar Cubo Medina <[email protected]>
*
* 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 <[email protected]>
*
* @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');
}
}
45 changes: 45 additions & 0 deletions src/DiagnosisCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of "Email Address" component.
*
* © Oscar Cubo Medina <[email protected]>
*
* 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 <[email protected]>
*
* @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');
}
}
42 changes: 42 additions & 0 deletions src/Exception/InvalidEmailAddressException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of "Email Address" component.
*
* © Oscar Cubo Medina <[email protected]>
*
* 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 <[email protected]>
*/
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
);
}
}
176 changes: 176 additions & 0 deletions src/Meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php

/*
* This file is part of "Email Address" component.
*
* © Oscar Cubo Medina <[email protected]>
*
* 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 <[email protected]>
*
* @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'];
}
}
7 changes: 4 additions & 3 deletions tests/AddressTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Ocubom\Email\Tests;

use Ocubom\Email\Address as EmailAddress;
use Ocubom\Email\Diagnosis;

/**
* Email Address Tests
Expand All @@ -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('[email protected]', (string) $obj, 'Email address must convert to string');
Expand All @@ -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('[email protected]', (string) $obj, 'Email address must convert to string');
Expand All @@ -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
*/
Expand Down
Loading

0 comments on commit 5512a22

Please sign in to comment.