Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteel committed Mar 18, 2020
0 parents commit 80c46ec
Show file tree
Hide file tree
Showing 35 changed files with 1,423 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.github export-ignore
/composer.lock export-ignore
/phpcs.xml export-ignore
/phpunit.xml.dist export-ignore
/tests/ export-ignore
44 changes: 44 additions & 0 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: PHPUnit Test Suite
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
run-tests:
name: PHP ${{ matrix.php-versions }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-versions: ['7.3', '7.4']
steps:
- uses: actions/checkout@v2
- name: Setup PHP Action
uses: shivammathur/setup-php@1.6.1
with:
php-version: ${{ matrix.php-versions }}
coverage: pcov
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-versions }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest --ignore-platform-reqs
- name: Check CS
run: composer cs-check
- name: PHPUnit
run: php -dpcov.directory=. -dpcov.enabled=1 -dpcov.exclude="~vendor~" ./vendor/bin/phpunit --stop-on-error --coverage-clover=coverage.xml
- name: Upload Coverage to CodeCov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
fail_ci_if_error: false
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/vendor/
/build/
composer.lock
phpunit.xml
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Laminas Mail Utilities

### Introduction

This is a small package to scratch two primary itches

- Validate `Laminas\Mail\Message` instances according to configurable constraints such as a maximum number of recipients, or a non-empty subject line for example.
- Provide simple behaviours and interfaces for email messages that should be sent via popular transactional or marketing email service providers; for example, key/value metadata or tagging/categorisation.

### Installation

```bash
composer require netglue/laminas-mail-utils
```

### Shipped Validators

Validators can be used as part of your input filter setup, or by creating vendor specific validator chains. Here's an example of a concrete validator chain that would be helpful validating messages to be sent via the Postmark API, which imposes certain restrictions on recipients etc:

```php
<?php
declare(strict_types=1);

namespace App;

use Netglue\Mail\Validator\HasFromAddress;
use Netglue\Mail\Validator\HasSubject;
use Netglue\Mail\Validator\HasToRecipient;
use Netglue\Mail\Validator\TotalFromCount;
use Netglue\Mail\Validator\TotalRecipientCount;
use Netglue\Mail\Validator\TotalReplyToCount;
use Laminas\Validator\ValidatorChain;
use Laminas\Validator\ValidatorPluginManager;

final class PostmarkMessageValidator extends ValidatorChain
{
private const MAX_RECIPIENTS = 50;

public function __construct(?ValidatorPluginManager $pluginManager = null)
{
parent::__construct();
if ($pluginManager) {
$this->setPluginManager($pluginManager);
}

$this->configureDefaults();
}

private function configureDefaults() : void
{
$this->attachByName(HasFromAddress::class);
$this->attachByName(TotalFromCount::class, ['max' => 1]);
$this->attachByName(HasSubject::class);
$this->attachByName(HasToRecipient::class);
$this->attachByName(TotalRecipientCount::class, ['max' => self::MAX_RECIPIENTS]);
$this->attachByName(TotalReplyToCount::class, ['max' => 1]);
}
}

```

### Shipped Traits & Interfaces

You'll find a collection of interfaces that are pretty minimal but hopefully encapsulate what a lot of transactional email service providers offer when it comes to additional features WRT sending mail - for example, assigning tags or categories to individual messages or turning click tracking or open tracking on and off.

Typically, you make a concrete descendant of `Laminas\Mail\Message` and implement whichever interfaces suit the provider best so that you have type safety when working with the capabilities of any given message instance. Again, using Postmark as an example, perhaps something like this:

```php
<?php
declare(strict_types=1);

namespace App\Postmark\Message;

use Netglue\Mail\Message\KeyValueMetadata;
use Netglue\Mail\Message\KeyValueMetadataBehaviour;
use Netglue\Mail\Message\OpenTracking;
use Netglue\Mail\Message\OpenTrackingBehaviour;
use Netglue\Mail\Message\TaggableMessage;
use Netglue\Mail\Message\TaggableMessageBehaviour;
use Laminas\Mail\Message;

class MyPostmarkMessage extends Message implements TaggableMessage, KeyValueMetadata, OpenTracking
{
use OpenTrackingBehaviour;
use TaggableMessageBehaviour;
use KeyValueMetadataBehaviour {
addMetaData as parentAddMetaData;
}
}
```

```php
// $message is given to us from some factory or service somewhere
if ($message instanceof TaggableMessage) {
$someVendorApi->setMessageTag($message->getTag());
}
```

### The plan…

… is to implement vendor specific packages that lever these utilities, with, as you may have guessed, Postmark at the top of the list currently…

The package is currently immature and subject to probable BC breaks and contributions are welcomed if this scratches any itches for you 👍



54 changes: 54 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "netglue/laminas-mail-utils",
"description": "A collection of straight-forward validators for Laminas mail messages and some common behaviours for transactional email messages",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "George Steel",
"email": "george@net-glue.co.uk"
}
],
"config": {
"sort-packages": true
},
"require": {
"php": ">=7.3",
"laminas/laminas-mail": "^2.10",
"laminas/laminas-mime": "^2.7",
"laminas/laminas-validator": "^2.13"
},
"autoload": {
"psr-4": {
"Netglue\\Mail\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Netglue\\MailTest\\": "tests"
}
},
"require-dev": {
"doctrine/coding-standard": "^7.0",
"laminas/laminas-component-installer": "^2.1",
"laminas/laminas-config-aggregator": "^1.2",
"laminas/laminas-servicemanager": "^3.4",
"phpunit/phpunit": "^9.0",
"roave/security-advisories": "dev-master"
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-html build/report"
},
"extra": {
"laminas": {
"component": "Netglue\\Mail\\ConfigProvider"
}
}
}
30 changes: 30 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<ruleset name="Custom Coding Standard based on Doctrine">
<arg name="basepath" value="."/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>
<arg name="cache" value=".php_cs.cache"/>
<arg name="colors"/>

<!-- Ignore warnings, show progress of the run and show sniff names -->
<arg value="nps"/>

<!-- Paths to check -->
<file>src</file>
<file>tests</file>

<!-- Inherit rules from Doctrine Coding Standard -->
<rule ref="Doctrine">
<exclude name="SlevomatCodingStandard.TypeHints.DeclareStrictTypes.IncorrectWhitespaceBetweenOpenTagAndDeclare"/>
<!-- I like return types to look like this: ") :? Type" or ") : Type" -->
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing.WhitespaceAfterNullabilitySymbol" />
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing.NoSpaceBetweenColonAndNullabilitySymbol" />

<!-- Whilst this lib is compatible with 7.3, exclude this sniff -->
<exclude name="SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingNativeTypeHint" />
</rule>

<rule ref="Generic.Formatting.MultipleStatementAlignment.NotSame">
<severity>0</severity>
</rule>
</ruleset>
17 changes: 17 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit Tests">
<directory>./tests</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>
32 changes: 32 additions & 0 deletions src/ConfigProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace Netglue\Mail;

use Laminas\ServiceManager\Factory\InvokableFactory;

class ConfigProvider
{
/** @return mixed[] */
public function __invoke() : array
{
return [
'validators' => $this->validators(),
];
}

/** @return mixed[] */
private function validators() : array
{
return [
'factories' => [
Validator\HasFromAddress::class => InvokableFactory::class,
Validator\HasSubject::class => InvokableFactory::class,
Validator\HasToRecipient::class => InvokableFactory::class,
Validator\TotalFromCount::class => InvokableFactory::class,
Validator\TotalRecipientCount::class => InvokableFactory::class,
Validator\TotalReplyToCount::class => InvokableFactory::class,
],
];
}
}
10 changes: 10 additions & 0 deletions src/Exception/InvalidArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

namespace Netglue\Mail\Exception;

use InvalidArgumentException;

class InvalidArgument extends InvalidArgumentException
{
}
14 changes: 14 additions & 0 deletions src/Message/KeyValueMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

namespace Netglue\Mail\Message;

interface KeyValueMetadata
{
/**
* Return key:value metadata for the email message
*
* @return mixed[]
*/
public function getMetaData() : iterable;
}
49 changes: 49 additions & 0 deletions src/Message/KeyValueMetadataBehaviour.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

namespace Netglue\Mail\Message;

use Netglue\Mail\Exception\InvalidArgument;
use function gettype;
use function is_scalar;
use function sprintf;

trait KeyValueMetadataBehaviour
{
/** @var mixed[] */
private $metaData = [];

/** @param mixed $value */
public function addMetaData(string $key, $value) : void
{
if (empty($key)) {
throw new InvalidArgument(
'Metadata keys cannot be empty'
);
}

if (! is_scalar($value) && $value !== null) {
throw new InvalidArgument(sprintf(
'Expected the metadata value to be scalar. Received %s.',
gettype($value)
));
}

$this->metaData[$key] = $value;
}

/** @param mixed[] $metadata */
public function setMetaData(iterable $metadata) : void
{
$this->metaData = [];
foreach ($metadata as $key => $value) {
$this->addMetaData($key, $value);
}
}

/** @return mixed[] $value */
public function getMetaData() : iterable
{
return $this->metaData;
}
}
14 changes: 14 additions & 0 deletions src/Message/MultipleTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

namespace Netglue\Mail\Message;

interface MultipleTags
{
/**
* Return the tags for email messages that can be tagged with multiple different 'tags'
*
* @return iterable|string[]
*/
public function getTags() : iterable;
}
Loading

0 comments on commit 80c46ec

Please sign in to comment.