Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,24 @@ jobs:

- name: Run lint
run: composer lint

phpstan:
runs-on: ubuntu-latest

name: PHPStan

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.5
coverage: none

- name: Install Composer dependencies
uses: ramsey/composer-install@v3

- name: Run phpstan
run: composer phpstan
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"illuminate/support": "^9.0 || ^10.0 || ^11.0 || ^12.0",
"knplabs/gaufrette": "^0.11",
"phpunit/phpunit": "^9.5",
"symfony/validator": "^5.4 || ^6.4 || ^7.0"
"symfony/validator": "^5.4 || ^6.4 || ^7.0",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0"
},
"suggest": {
"symfony/validator": "Add validation constraints",
Expand Down Expand Up @@ -49,6 +51,9 @@
],
"lint": [
"php-cs-fixer fix --ansi"
],
"phpstan": [
"phpstan analyse"
]
}
}
10 changes: 10 additions & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
level: 8
paths:
- src/
- tests/
13 changes: 5 additions & 8 deletions src/EmailChecker/Adapter/AgregatorAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,18 @@
*/
class AgregatorAdapter implements AdapterInterface
{
/**
* @var AdapterInterface[]
*/
protected $adapters;

/**
* Build agregator adapter with a list of adpaters (order matters).
* Build aggregator adapter with a list of adapters (order matters).
*
* @param array $adapters List of AdapterInterface objects
* @param AdapterInterface[] $adapters List of AdapterInterface objects
*/
public function __construct(array $adapters)
{
foreach ($adapters as $adapter) {
if (!$adapter instanceof AdapterInterface) {
throw new \InvalidArgumentException('AgregatorAdapter only accept instances of AdapterInterface');
}
}

$this->adapters = $adapters;
}

Expand Down
5 changes: 4 additions & 1 deletion src/EmailChecker/Adapter/ArrayAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
*/
class ArrayAdapter implements AdapterInterface
{
/**
* @var string[]
*/
protected $domains;

/**
* @param array $domains List of throwaway domains
* @param string[] $domains List of throwaway domains
*/
public function __construct(array $domains)
{
Expand Down
8 changes: 7 additions & 1 deletion src/EmailChecker/Adapter/BuiltInAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@
*/
class BuiltInAdapter implements AdapterInterface
{
protected $domains;
/**
* @var string[]|null
*/
protected $domains = null;

public function isThrowawayDomain($domain)
{
return in_array($domain, $this->getDomains());
}

/**
* @return string[]
*/
private function getDomains()
{
if (null === $this->domains) {
Expand Down
8 changes: 6 additions & 2 deletions src/EmailChecker/Adapter/FileAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@
*/
class FileAdapter implements AdapterInterface
{
/**
* @var string[]
*/
protected $domains;

/**
* @param string $filename Filename containing all domains
*/
public function __construct($filename)
{
if (!file_exists($filename)) {
$content = file_get_contents($filename);
if (false === $content) {
throw new \InvalidArgumentException(sprintf('File "%s" not found', $filename));
}

$this->domains = Utilities::parseLines(file_get_contents($filename));
$this->domains = Utilities::parseLines($content);
}

public function isThrowawayDomain($domain)
Expand Down
3 changes: 3 additions & 0 deletions src/EmailChecker/Adapter/GaufretteAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
*/
class GaufretteAdapter implements AdapterInterface
{
/**
* @var string[]
*/
protected $domains;

public function __construct(File $file)
Expand Down
3 changes: 3 additions & 0 deletions src/EmailChecker/Constraints/NotThrowawayEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#[\Attribute]
class NotThrowawayEmail extends Constraint
{
/**
* @var string
*/
public $message = 'The domain associated with this email is not valid.';

public function __construct(
Expand Down
14 changes: 13 additions & 1 deletion src/EmailChecker/Constraints/NotThrowawayEmailValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,27 @@
*/
class NotThrowawayEmailValidator extends ConstraintValidator
{
/**
* @var EmailChecker
*/
protected $emailChecker;

public function __construct(?EmailChecker $emailChecker = null)
{
$this->emailChecker = $emailChecker ?: new EmailChecker();
}

/**
* @param mixed $value
*
* @return void
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof NotThrowawayEmail) {
throw new UnexpectedTypeException($constraint, NotThrowawayEmail::class);
}

if (null === $value || '' === $value) {
return;
}
Expand All @@ -38,7 +50,7 @@ public function validate($value, Constraint $constraint)
throw new UnexpectedTypeException($value, 'string');
}

if (!$this->emailChecker->isValid($value)) {
if (!$this->emailChecker->isValid((string) $value)) {
$this->context->addViolation($constraint->message);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/EmailChecker/EmailChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
*/
class EmailChecker
{
/**
* @var AdapterInterface
*/
protected $adapter;

/**
* @param AdapterInterface $adapter Checker adapter
*/
public function __construct(?AdapterInterface $adapter = null)
{
$this->adapter = $adapter ?: new BuiltInAdapter();
$this->adapter = $adapter ?? new BuiltInAdapter();
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/EmailChecker/Laravel/EmailCheckerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
*/
class EmailCheckerServiceProvider extends ServiceProvider
{
protected $app;

/**
* Register the factory in the application container.
*
* @return void
*/
public function register()
{
Expand All @@ -44,6 +44,8 @@ public function register()

/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(EmailChecker $checker)
{
Expand All @@ -62,7 +64,7 @@ public function boot(EmailChecker $checker)
/**
* Get the services provided by the provider.
*
* @return array
* @return array<string>
*/
public function provides()
{
Expand Down
17 changes: 14 additions & 3 deletions src/EmailChecker/ThrowawayDomains.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,29 @@
* List of built-in throwaway domains read from the resources folder.
*
* @author Matthieu Moquet <[email protected]>
*
* @implements \IteratorAggregate<string>
*/
class ThrowawayDomains implements \IteratorAggregate, \Countable
{
/**
* @var string[]
*/
protected $domains;

public function __construct()
{
$this->domains = Utilities::parseLines(file_get_contents(
__DIR__.'/../../res/throwaway_domains.txt'
));
$content = file_get_contents(__DIR__.'/../../res/throwaway_domains.txt');
if (false === $content) {
throw new \LogicException('File "throwaway_domains.txt" not found');
}

$this->domains = Utilities::parseLines($content);
}

/**
* @return string[]
*/
public function toArray()
{
return $this->domains;
Expand Down
17 changes: 6 additions & 11 deletions src/EmailChecker/Utilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,10 @@ class Utilities
*
* @param string $email The email address to parse
*
* @return array The parts of the email. First the local part, then the domain
* @return array{string, string} The parts of the email. First the local part, then the domain
*/
public static function parseEmailAddress($email)
{
if (!is_string($email)) {
throw new InvalidEmailException(sprintf('Expected a string, received %s', gettype($email)));
}

$pattern = sprintf('/^(?<local>%s)@(?<domain>%s)$/iD', self::EMAIL_REGEX_LOCAL, self::EMAIL_REGEX_DOMAIN);

if (!preg_match($pattern, $email, $parts)) {
Expand All @@ -50,7 +46,7 @@ public static function parseEmailAddress($email)
*
* @param string $content The content to parse
*
* @return array Array of cleaned string
* @return array<string> Array of cleaned string
*/
public static function parseLines($content)
{
Expand All @@ -62,10 +58,9 @@ public static function parseLines($content)
$lines = array_map('strtolower', $lines);

// Remove empty lines and comments
$lines = array_filter($lines, function ($line) {
return (0 === strlen($line) || '#' === $line[0]) ? false : $line;
});

return $lines;
return array_filter(
$lines,
static fn (string $line): bool => 0 !== strlen($line) && '#' !== $line[0],
);
}
}
32 changes: 11 additions & 21 deletions tests/EmailChecker/Tests/Adpater/AgregatorAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,36 @@
use EmailChecker\Adapter\AgregatorAdapter;
use EmailChecker\Tests\TestCase;

class AgregatorAdapterTest extends TestCase
final class AgregatorAdapterTest extends TestCase
{
public function testAllValid()
public function testAllValid(): void
{
$adapter1 = $this->getAdapterMock(false, 'once');
$adapter2 = $this->getAdapterMock(false, 'once');

$this->adapter = new AgregatorAdapter([$adapter1, $adapter2]);
$adapter = new AgregatorAdapter([$adapter1, $adapter2]);

$this->assertFalse($this->adapter->isThrowawayDomain('example.org'));
$this->assertFalse($adapter->isThrowawayDomain('example.org'));
}

public function testFirstInvalid()
public function testFirstInvalid(): void
{
$adapter1 = $this->getAdapterMock(true, 'once');
$adapter2 = $this->getAdapterMock(false, 'never');

$this->adapter = new AgregatorAdapter([$adapter1, $adapter2]);
$adapter = new AgregatorAdapter([$adapter1, $adapter2]);

$this->assertTrue($this->adapter->isThrowawayDomain('example.org'));
$this->assertTrue($adapter->isThrowawayDomain('example.org'));
}

public function testSecondInvalid()
public function testSecondInvalid(): void
{
$adapter1 = $this->getAdapterMock(false, 'once');
$adapter2 = $this->getAdapterMock(true, 'once');

$this->adapter = new AgregatorAdapter([$adapter1, $adapter2]);
$adapter = new AgregatorAdapter([$adapter1, $adapter2]);

$this->assertTrue($this->adapter->isThrowawayDomain('example.org'));
}

public function testCheckArrayValuesInstanceOf()
{
$this->expectException(\InvalidArgumentException::class);

new AgregatorAdapter([
new \stdClass(),
new \stdClass(),
]);
$this->assertTrue($adapter->isThrowawayDomain('example.org'));
}

/**
Expand All @@ -64,7 +54,7 @@ public function testCheckArrayValuesInstanceOf()
*
* @return AdapterInterface The mock adapter
*/
protected function getAdapterMock($isThrowawayDomain, $call = 'any')
protected function getAdapterMock(bool $isThrowawayDomain, string $call = 'any'): AdapterInterface
{
$adapter = $this->createMock(AdapterInterface::class);
$adapter->expects($this->$call())
Expand Down
Loading