<?php

/*
 * This file is part of the Typhoon package.
 *
 * Copyright © 2013 Erin Millard
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eloquent\Typhoon\Generator;

use Eloquent\Cosmos\ClassName;
use Eloquent\Typhax\Renderer\TypeRenderer;
use Eloquent\Typhax\Type\Type;
use Eloquent\Typhoon\Parameter\Parameter;
use Eloquent\Typhoon\Parameter\ParameterList;
use Eloquent\Typhoon\Parameter\Visitor;
use Eloquent\Typhoon\TypeCheck\TypeCheck;
use Icecave\Pasta\AST\Expr\Assign;
use Icecave\Pasta\AST\Expr\Call;
use Icecave\Pasta\AST\Expr\Greater;
use Icecave\Pasta\AST\Expr\Less;
use Icecave\Pasta\AST\Expr\Literal;
use Icecave\Pasta\AST\Expr\LogicalNot;
use Icecave\Pasta\AST\Expr\NewOperator;
use Icecave\Pasta\AST\Expr\PostfixIncrement;
use Icecave\Pasta\AST\Expr\QualifiedIdentifier;
use Icecave\Pasta\AST\Expr\Subscript;
use Icecave\Pasta\AST\Expr\Variable;
use Icecave\Pasta\AST\Func\Closure;
use Icecave\Pasta\AST\Func\Parameter as ParameterASTNode;
use Icecave\Pasta\AST\Stmt\ExpressionStatement;
use Icecave\Pasta\AST\Stmt\ForStatement;
use Icecave\Pasta\AST\Stmt\IfStatement;
use Icecave\Pasta\AST\Stmt\IStatement;
use Icecave\Pasta\AST\Stmt\StatementBlock;
use Icecave\Pasta\AST\Stmt\ThrowStatement;
use Icecave\Pasta\AST\Identifier;

class ParameterListGenerator implements Visitor
{
    /**
     * @param TyphaxASTGenerator|null $typeGenerator
     * @param TypeRenderer|null       $typeRenderer
     */
    public function __construct(
        TyphaxASTGenerator $typeGenerator = null,
        TypeRenderer $typeRenderer = null
    ) {
        $this->typeCheck = TypeCheck::get(__CLASS__, func_get_args());

        if (null === $typeGenerator) {
            $typeGenerator = new TyphaxASTGenerator;
        }
        if (null === $typeRenderer) {
            $typeRenderer = new TypeRenderer;
        }

        $this->typeGenerator = $typeGenerator;
        $this->typeRenderer = $typeRenderer;

        $this->argumentExpression = new Variable(new Identifier('argument'));
        $this->indexExpression = new Variable(new Identifier('index'));
        $this->validatorNamespace = ClassName::fromAtoms(array('Typhoon'), true);
    }

    /**
     * @return TyphaxASTGenerator
     */
    public function typeGenerator()
    {
        $this->typeCheck->typeGenerator(func_get_args());

        return $this->typeGenerator;
    }

    /**
     * @return TypeRenderer
     */
    public function typeRenderer()
    {
        $this->typeCheck->typeRenderer(func_get_args());

        return $this->typeRenderer;
    }

    /**
     * @param ClassName $validatorNamespace
     */
    public function setValidatorNamespace(ClassName $validatorNamespace)
    {
        $this->typeCheck->setValidatorNamespace(func_get_args());

        $this->validatorNamespace = $validatorNamespace->toAbsolute();
    }

    /**
     * @return ClassName
     */
    public function validatorNamespace()
    {
        $this->typeCheck->validatorNamespace(func_get_args());

        return $this->validatorNamespace;
    }

    /**
     * @param Parameter $parameter
     *
     * @return array<integer,IStatement>
     */
    public function visitParameter(Parameter $parameter)
    {
        $this->typeCheck->visitParameter(func_get_args());

        $typeExpression = $parameter->type()->accept($this->typeGenerator());
        if (null === $typeExpression) {
            return array();
        }

        $expressions = array();
        if ($typeExpression instanceof Closure) {
            $checkVariable = new Variable(new Identifier('check'));
            $expressions[] = new ExpressionStatement(new Assign(
                $checkVariable,
                $typeExpression
            ));
            $conditionExpression = new Call($checkVariable);
            $conditionExpression->add($this->argumentExpression);
        } else {
            $conditionExpression = $typeExpression;
        }

        $newExceptionCall = new Call(QualifiedIdentifier::fromString(
            $this->validatorNamespace()->joinAtoms(
                'Exception',
                'UnexpectedArgumentValueException'
            )->string()
        ));
        $newExceptionCall->add(new Literal($parameter->name()));
        $newExceptionCall->add($this->indexExpression);
        $newExceptionCall->add($this->argumentExpression);
        $newExceptionCall->add(new Literal(
            $this->renderTypeName($parameter->type())
        ));
        $ifStatement = new IfStatement(new LogicalNot($conditionExpression));
        $ifStatement->trueBranch()->add(
            new ThrowStatement(new NewOperator($newExceptionCall))
        );
        $expressions[] = $ifStatement;

        return $expressions;
    }

    /**
     * @param ParameterList $parameterList
     *
     * @return array<integer,IStatement>
     */
    public function visitParameterList(ParameterList $parameterList)
    {
        $this->typeCheck->visitParameterList(func_get_args());

        $expressions = array();
        $parameters = $parameterList->parameters();
        $parameterCount = count($parameters);
        $argumentsVariable = new Variable(new Identifier('arguments'));

        // empty parameter list
        if ($parameterCount < 1) {
            $zeroLiteral = new Literal(0);
            $countCall = new Call(QualifiedIdentifier::fromString('\count'));
            $countCall->add($argumentsVariable);
            $newExceptionCall = new Call(QualifiedIdentifier::fromString(
                $this->validatorNamespace()->joinAtoms(
                    'Exception',
                    'UnexpectedArgumentException'
                )->string()
            ));
            $newExceptionCall->add($zeroLiteral);
            $newExceptionCall->add(new Subscript(
                $argumentsVariable,
                $zeroLiteral
            ));
            $ifStatement = new IfStatement(new Greater($countCall, $zeroLiteral));
            $ifStatement->trueBranch()->add(
                new ThrowStatement(new NewOperator($newExceptionCall))
            );
            $expressions[] = $ifStatement;

            return $this->wrapExpressions($expressions);
        }

        $argumentCountVariable = new Variable(new Identifier('argumentCount'));
        $argumentCountCall = new Call(QualifiedIdentifier::fromString('\count'));
        $argumentCountCall->add($argumentsVariable);
        $expressions[] = new Assign(
            $argumentCountVariable,
            $argumentCountCall
        );

        // missing parameter checks
        $requiredParameterCount = count($parameterList->requiredParameters());
        $lastRequiredParameterIndex = $requiredParameterCount - 1;
        $missingParametersStatement = null;
        if ($requiredParameterCount > 0) {
            $missingParametersStatement = new IfStatement(
                new Less($argumentCountVariable, new Literal($requiredParameterCount))
            );
            for ($i = 0; $i < $lastRequiredParameterIndex; $i ++) {
                $newExceptionCall = new Call(QualifiedIdentifier::fromString(
                    $this->validatorNamespace()->joinAtoms(
                        'Exception',
                        'MissingArgumentException'
                    )->string()
                ));
                $newExceptionCall->add(new Literal($parameters[$i]->name()));
                $newExceptionCall->add(new Literal($i));
                $newExceptionCall->add(new Literal(
                    $this->renderTypeName($parameters[$i]->type())
                ));
                $ifStatement = new IfStatement(
                    new Less($argumentCountVariable, new Literal($i + 1))
                );
                $ifStatement->trueBranch()->add(
                    new ThrowStatement(new NewOperator($newExceptionCall))
                );
                $missingParametersStatement->trueBranch()->add($ifStatement);
            }
            $newExceptionCall = new Call(QualifiedIdentifier::fromString(
                $this->validatorNamespace()->joinAtoms(
                    'Exception',
                    'MissingArgumentException'
                )->string()
            ));
            $newExceptionCall->add(new Literal(
                $parameters[$lastRequiredParameterIndex]->name()
            ));
            $newExceptionCall->add(new Literal(
                $lastRequiredParameterIndex
            ));
            $newExceptionCall->add(new Literal(
                $this->renderTypeName($parameters[$lastRequiredParameterIndex]->type())
            ));
            $missingParametersStatement->trueBranch()->add(
                new ThrowStatement(new NewOperator($newExceptionCall))
            );
        }

        // unexpected arguments check
        if (!$parameterList->isVariableLength()) {
            $parameterCountLiteral = new Literal($parameterCount);
            $newExceptionCall = new Call(QualifiedIdentifier::fromString(
                $this->validatorNamespace()->joinAtoms(
                    'Exception',
                    'UnexpectedArgumentException'
                )->string()
            ));
            $newExceptionCall->add($parameterCountLiteral);
            $newExceptionCall->add(new Subscript(
                $argumentsVariable,
                $parameterCountLiteral
            ));
            $tooManyParametersStatement = new IfStatement(
                new Greater($argumentCountVariable, $parameterCountLiteral)
            );
            $tooManyParametersStatement->trueBranch()->add(
                new ThrowStatement(new NewOperator($newExceptionCall))
            );

            if ($missingParametersStatement) {
                $missingParametersStatement->setFalseBranch(
                    $tooManyParametersStatement
                );
            } else {
                $expressions[] = $tooManyParametersStatement;
            }
        }

        if ($missingParametersStatement) {
            $expressions[] = $missingParametersStatement;
        }

        // type checks
        foreach ($parameters as $index => $parameter) {
            $isVariableLength =
                $parameterList->isVariableLength() &&
                $index === $parameterCount - 1
            ;

            $indexLiteral = new Literal($index);
            $oldArgumentExpression = $this->argumentExpression;
            $oldIndexExpression = $this->indexExpression;

            if (!$isVariableLength) {
                $this->indexExpression = $indexLiteral;
                $this->argumentExpression = new Subscript(
                    $argumentsVariable,
                    $this->indexExpression
                );
            }

            $parameterExpressions = $parameter->accept($this);
            if (count($parameterExpressions) < 1) {
                $this->argumentExpression = $oldArgumentExpression;
                $this->indexExpression = $oldIndexExpression;

                continue;
            }
            array_unshift(
                $parameterExpressions,
                new ExpressionStatement(new Assign(
                    new Variable(new Identifier('value')),
                    $this->argumentExpression
                ))
            );
            $parameterExpressions = $this->wrapExpressions($parameterExpressions);

            // wrap variable length in loop
            if ($isVariableLength) {
                $closure = new Closure;
                $closure->addParameter(new ParameterASTNode(new Identifier('argument')));
                $closure->addParameter(new ParameterASTNode(new Identifier('index')));
                foreach ($parameterExpressions as $expression) {
                    $closure->statementBlock()->add($expression);
                }

                $checkVariable = new Variable(new Identifier('check'));
                $indexVariable = new Variable(new Identifier('index'));
                $checkCall = new Call($checkVariable);
                $checkCall->add(new Subscript($argumentsVariable, $indexVariable));
                $checkCall->add($indexVariable);
                $loopContents = new StatementBlock;
                $loopContents->add(new ExpressionStatement($checkCall));

                $parameterExpressions = array(
                    new ExpressionStatement(new Assign($checkVariable, $closure)),
                    new ForStatement(
                        new Assign($indexVariable, $indexLiteral),
                        new Less($indexVariable, $argumentCountVariable),
                        new PostfixIncrement($indexVariable),
                        $loopContents
                    ),
                );
            }

            // wrap optional in if statement
            if ($parameter->isOptional()) {
                $if = new IfStatement(new Greater($argumentCountVariable, $indexLiteral));
                foreach ($parameterExpressions as $expression) {
                    $if->trueBranch()->add($expression);
                }
                $parameterExpressions = array($if);
            }

            foreach ($parameterExpressions as $expression) {
                $expressions[] = $expression;
            }

            $this->argumentExpression = $oldArgumentExpression;
            $this->indexExpression = $oldIndexExpression;
        }

        return $this->wrapExpressions($expressions);
    }

    /**
     * @param array $expressions
     *
     * @return array<integer,IStatement>
     */
    protected function wrapExpressions(array $expressions)
    {
        $this->typeCheck->wrapExpressions(func_get_args());

        foreach ($expressions as $index => $expression) {
            if (!$expression instanceof IStatement) {
                $expressions[$index] = new ExpressionStatement($expression);
            }
        }

        return $expressions;
    }

    /**
     * @param Type $type
     *
     * @return string
     */
    protected function renderTypeName(Type $type)
    {
        $this->typeCheck->renderTypeName(func_get_args());

        if ($type instanceof NullifiedType) {
            return $type->originalType()->accept($this->typeRenderer());
        }

        return $type->accept($this->typeRenderer());
    }

    private $typeGenerator;
    private $typeRenderer;
    private $argumentExpression;
    private $indexExpression;
    private $validatorNamespace;
    private $typeCheck;
}


<?php

/*
 * This file is part of the Typhoon package.
 *
 * Copyright © 2013 Erin Millard
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eloquent\Typhoon\Generator;

use Eloquent\Cosmos\ClassName;
use Eloquent\Typhoon\Configuration\RuntimeConfiguration;
use Eloquent\Typhoon\TypeCheck\TypeCheck;
use Icecave\Pasta\AST\Expr\QualifiedIdentifier;
use Icecave\Pasta\AST\Func\ArrayTypeHint;
use Icecave\Pasta\AST\Func\Parameter;
use Icecave\Pasta\AST\Identifier;
use Icecave\Pasta\AST\PhpBlock;
use Icecave\Pasta\AST\Stmt\NamespaceStatement;
use Icecave\Pasta\AST\SyntaxTree;
use Icecave\Pasta\AST\Type\AccessModifier;
use Icecave\Pasta\AST\Type\ClassDefinition;
use Icecave\Pasta\AST\Type\ConcreteMethod;
use Icecave\Rasta\Renderer;

class DummyValidatorGenerator implements StaticClassGenerator
{
    /**
     * @param Renderer|null $renderer
     */
    public function __construct(Renderer $renderer = null)
    {
        $this->typeCheck = TypeCheck::get(__CLASS__, func_get_args());

        if (null === $renderer) {
            $renderer = new Renderer;
        }

        $this->renderer = $renderer;
    }

    /**
     * @return Renderer
     */
    public function renderer()
    {
        $this->typeCheck->renderer(func_get_args());

        return $this->renderer;
    }

    /**
     * @param RuntimeConfiguration $configuration
     * @param null                 &$className
     *
     * @return string
     */
    public function generate(
        RuntimeConfiguration $configuration,
        &$className = null
    ) {
        $this->typeCheck->generate(func_get_args());

        return $this->generateSyntaxTree(
            $configuration,
            $className
        )->accept($this->renderer());
    }

    /**
     * @param RuntimeConfiguration $configuration
     * @param null                 &$className
     *
     * @return SyntaxTree
     */
    public function generateSyntaxTree(
        RuntimeConfiguration $configuration,
        &$className = null
    ) {
        $this->typeCheck->generateSyntaxTree(func_get_args());

        $className = $configuration
            ->validatorNamespace()
            ->joinAtoms('DummyValidator')
        ;

        $classDefinition = new ClassDefinition(
            new Identifier($className->shortName()->string())
        );
        $classDefinition->setParentName(
            QualifiedIdentifier::fromString('AbstractValidator')
        );
        $classDefinition->add($this->generateCallMethod());

        $primaryBlock = new PhpBlock;
        $primaryBlock->add(new NamespaceStatement(QualifiedIdentifier::fromString(
            $className->parent()->toRelative()->string()
        )));
        $primaryBlock->add($classDefinition);

        $syntaxTree = new SyntaxTree;
        $syntaxTree->add($primaryBlock);

        return $syntaxTree;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateCallMethod()
    {
        $this->typeCheck->generateCallMethod(func_get_args());

        $method = new ConcreteMethod(
            new Identifier('__call'),
            AccessModifier::PUBLIC_()
        );
        $method->addParameter(new Parameter(new Identifier('name')));
        $method->addParameter(new Parameter(
            new Identifier('arguments'),
            new ArrayTypeHint
        ));

        return $method;
    }

    private $renderer;
    private $typeCheck;
}


<?php

/**
 * This is responsible for filtering only published posts to users who do not have
 * permission to view non-published posts.
 *
 * @package silverstripe
 * @subpackage blog
 *
 * @author Michael Strong <github@michaelstrong.co.uk>
**/
class BlogPostFilter extends DataExtension {

	/**
	 * Augment queries so that we don't fetch unpublished articles.
	**/
	public function augmentSQL(SQLQuery &$query) {
		$stage = Versioned::current_stage();
		if($stage == 'Live' || !Permission::check("VIEW_DRAFT_CONTENT")) {
			$query->addWhere("PublishDate < '" . Convert::raw2sql(SS_Datetime::now()) . "'");
		}
	}

	/**
	 * This is a fix so that when we try to fetch subclasses of BlogPost,
	 * lazy loading includes the BlogPost table in its query. Leaving this table out
	 * means the default sort order column PublishDate causes an error.
	 *
	 * @see https://github.com/silverstripe/silverstripe-framework/issues/1682
	**/
	public function augmentLoadLazyFields(SQLQuery &$query, &$dataQuery, $parent) {
		// Ensures that we're joining the BlogPost table which holds required db fields.
		$dataQuery->innerJoin("BlogPost", "`SiteTree`.`ID` = `BlogPost`.`ID`");
	}

}


<?php

/*
 * This file is part of the Typhoon package.
 *
 * Copyright © 2013 Erin Millard
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eloquent\Typhoon\Generator;

use Eloquent\Cosmos\ClassName;
use Eloquent\Typhoon\Configuration\RuntimeConfiguration;
use Eloquent\Typhoon\TypeCheck\TypeCheck;
use Icecave\Pasta\AST\Expr\ArrayLiteral;
use Icecave\Pasta\AST\Expr\Assign;
use Icecave\Pasta\AST\Expr\Call;
use Icecave\Pasta\AST\Expr\Constant;
use Icecave\Pasta\AST\Expr\GreaterEqual;
use Icecave\Pasta\AST\Expr\InstanceOfType;
use Icecave\Pasta\AST\Expr\Less;
use Icecave\Pasta\AST\Expr\Literal;
use Icecave\Pasta\AST\Expr\Member;
use Icecave\Pasta\AST\Expr\NewOperator;
use Icecave\Pasta\AST\Expr\QualifiedIdentifier;
use Icecave\Pasta\AST\Expr\PostfixIncrement;
use Icecave\Pasta\AST\Expr\StrictEquals;
use Icecave\Pasta\AST\Expr\Subscript;
use Icecave\Pasta\AST\Expr\Variable;
use Icecave\Pasta\AST\Func\ArrayTypeHint;
use Icecave\Pasta\AST\Func\Parameter;
use Icecave\Pasta\AST\Identifier;
use Icecave\Pasta\AST\PhpBlock;
use Icecave\Pasta\AST\Stmt\BreakStatement;
use Icecave\Pasta\AST\Stmt\ExpressionStatement;
use Icecave\Pasta\AST\Stmt\ForeachStatement;
use Icecave\Pasta\AST\Stmt\IfStatement;
use Icecave\Pasta\AST\Stmt\NamespaceStatement;
use Icecave\Pasta\AST\Stmt\ReturnStatement;
use Icecave\Pasta\AST\Stmt\StatementBlock;
use Icecave\Pasta\AST\SyntaxTree;
use Icecave\Pasta\AST\Type\AccessModifier;
use Icecave\Pasta\AST\Type\ClassDefinition;
use Icecave\Pasta\AST\Type\ConcreteMethod;
use Icecave\Rasta\Renderer;

class TypeInspectorGenerator implements StaticClassGenerator
{
    /**
     * @param Renderer|null $renderer
     */
    public function __construct(Renderer $renderer = null)
    {
        $this->typeCheck = TypeCheck::get(__CLASS__, func_get_args());

        if (null === $renderer) {
            $renderer = new Renderer;
        }

        $this->renderer = $renderer;
    }

    /**
     * @return Renderer
     */
    public function renderer()
    {
        $this->typeCheck->renderer(func_get_args());

        return $this->renderer;
    }

    /**
     * @param RuntimeConfiguration $configuration
     * @param null                 &$className
     *
     * @return string
     */
    public function generate(
        RuntimeConfiguration $configuration,
        &$className = null
    ) {
        $this->typeCheck->generate(func_get_args());

        return $this->generateSyntaxTree(
            $configuration,
            $className
        )->accept($this->renderer());
    }

    /**
     * @param RuntimeConfiguration $configuration
     * @param null                 &$className
     *
     * @return SyntaxTree
     */
    public function generateSyntaxTree(
        RuntimeConfiguration $configuration,
        &$className = null
    ) {
        $this->typeCheck->generateSyntaxTree(func_get_args());

        $className = $configuration
            ->validatorNamespace()
            ->joinAtoms('TypeInspector')
        ;

        $classDefinition = new ClassDefinition(
            new Identifier($className->shortName()->string())
        );
        $classDefinition->add($this->generateTypeMethod());
        $classDefinition->add($this->generateArrayTypeMethod());
        $classDefinition->add($this->generateObjectTypeMethod());
        $classDefinition->add($this->generateTraversableSubTypesMethod());
        $classDefinition->add($this->generateResourceTypeMethod());
        $classDefinition->add($this->generateStreamTypeMethod());

        $primaryBlock = new PhpBlock;
        $primaryBlock->add(new NamespaceStatement(QualifiedIdentifier::fromString(
            $className->parent()->toRelative()->string()
        )));
        $primaryBlock->add($classDefinition);

        $syntaxTree = new SyntaxTree;
        $syntaxTree->add($primaryBlock);

        return $syntaxTree;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateTypeMethod()
    {
        $this->typeCheck->generateTypeMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $maxIterationsIdentifier = new Identifier('maxIterations');
        $maxIterationsVariable = new Variable($maxIterationsIdentifier);
        $nativeTypeVariable = new Variable(new Identifier('nativeType'));
        $thisVariable = new Variable(new Identifier('this'));

        $method = new ConcreteMethod(
            new Identifier('type'),
            AccessModifier::PUBLIC_()
        );
        $method->addParameter(new Parameter($valueIdentifier));
        $maxIterationsParameter = new Parameter($maxIterationsIdentifier);
        $maxIterationsParameter->setDefaultValue(new Literal(10));
        $method->addParameter($maxIterationsParameter);

        $gettypeCall = new Call(QualifiedIdentifier::fromString('\gettype'));
        $gettypeCall->add($valueVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $nativeTypeVariable,
            $gettypeCall
        )));

        $arrayTypeIf = new IfStatement(new StrictEquals(
            new Literal('array'),
            $nativeTypeVariable
        ));
        $arrayTypeCall = new Call(new Member(
            $thisVariable,
            new Constant(new Identifier('arrayType'))
        ));
        $arrayTypeCall->add($valueVariable);
        $arrayTypeCall->add($maxIterationsVariable);
        $arrayTypeIf->trueBranch()->add(new ReturnStatement($arrayTypeCall));
        $method->statementBlock()->add($arrayTypeIf);

        $doubleTypeIf = new IfStatement(new StrictEquals(
            new Literal('double'),
            $nativeTypeVariable
        ));
        $doubleTypeIf->trueBranch()->add(new ReturnStatement(new Literal('float')));
        $method->statementBlock()->add($doubleTypeIf);

        $nullTypeIf = new IfStatement(new StrictEquals(
            new Literal('NULL'),
            $nativeTypeVariable
        ));
        $nullTypeIf->trueBranch()->add(new ReturnStatement(new Literal('null')));
        $method->statementBlock()->add($nullTypeIf);

        $objectTypeIf = new IfStatement(new StrictEquals(
            new Literal('object'),
            $nativeTypeVariable
        ));
        $objectTypeCall = new Call(new Member(
            $thisVariable,
            new Constant(new Identifier('objectType'))
        ));
        $objectTypeCall->add($valueVariable);
        $objectTypeCall->add($maxIterationsVariable);
        $objectTypeIf->trueBranch()->add(new ReturnStatement($objectTypeCall));
        $method->statementBlock()->add($objectTypeIf);

        $resourceTypeIf = new IfStatement(new StrictEquals(
            new Literal('resource'),
            $nativeTypeVariable
        ));
        $resourceTypeCall = new Call(new Member(
            $thisVariable,
            new Constant(new Identifier('resourceType'))
        ));
        $resourceTypeCall->add($valueVariable);
        $resourceTypeIf->trueBranch()->add(new ReturnStatement($resourceTypeCall));
        $method->statementBlock()->add($resourceTypeIf);

        $method->statementBlock()->add(new ReturnStatement($nativeTypeVariable));

        return $method;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateArrayTypeMethod()
    {
        $this->typeCheck->generateArrayTypeMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $maxIterationsIdentifier = new Identifier('maxIterations');
        $maxIterationsVariable = new Variable($maxIterationsIdentifier);

        $method = new ConcreteMethod(
            new Identifier('arrayType'),
            AccessModifier::PROTECTED_()
        );
        $method->addParameter(new Parameter(
            $valueIdentifier,
            new ArrayTypeHint
        ));
        $method->addParameter(new Parameter($maxIterationsIdentifier));

        $sprintfCall = new Call(QualifiedIdentifier::fromString('\sprintf'));
        $sprintfCall->add(new Literal('array%s'));
        $traversableSubTypesCall = new Call(new Member(
            new Variable(new Identifier('this')),
            new Constant(new Identifier('traversableSubTypes'))
        ));
        $traversableSubTypesCall->add($valueVariable);
        $traversableSubTypesCall->add($maxIterationsVariable);
        $sprintfCall->add($traversableSubTypesCall);
        $method->statementBlock()->add(new ReturnStatement($sprintfCall));

        return $method;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateObjectTypeMethod()
    {
        $this->typeCheck->generateObjectTypeMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $maxIterationsIdentifier = new Identifier('maxIterations');
        $maxIterationsVariable = new Variable($maxIterationsIdentifier);
        $reflectorVariable = new Variable(new Identifier('reflector'));
        $classVariable = new Variable(new Identifier('class'));
        $traversableSubTypesVariable = new Variable(new Identifier('traversableSubTypes'));
        $thisVariable = new Variable(new Identifier('this'));

        $method = new ConcreteMethod(
            new Identifier('objectType'),
            AccessModifier::PROTECTED_()
        );
        $method->addParameter(new Parameter($valueIdentifier));
        $method->addParameter(new Parameter($maxIterationsIdentifier));

        $newReflectorCall = new Call(
            QualifiedIdentifier::fromString('\ReflectionObject')
        );
        $newReflectorCall->add($valueVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $reflectorVariable,
            new NewOperator($newReflectorCall)
        )));

        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $classVariable,
            new Call(new Member(
                $reflectorVariable,
                new Constant(new Identifier('getName'))
            ))
        )));

        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $traversableSubTypesVariable,
            new Literal('')
        )));
        $traversableIf = new IfStatement(new InstanceOfType(
            $valueVariable,
            QualifiedIdentifier::fromString('\Traversable')
        ));
        $traversableSubTypesCall = new Call(new Member(
            $thisVariable,
            new Constant(new Identifier('traversableSubTypes'))
        ));
        $traversableSubTypesCall->add($valueVariable);
        $traversableSubTypesCall->add($maxIterationsVariable);
        $traversableIf->trueBranch()->add(new ExpressionStatement(new Assign(
            $traversableSubTypesVariable,
            $traversableSubTypesCall
        )));
        $method->statementBlock()->add($traversableIf);

        $sprintfCall = new Call(QualifiedIdentifier::fromString('\sprintf'));
        $sprintfCall->add(new Literal('%s%s'));
        $sprintfCall->add($classVariable);
        $sprintfCall->add($traversableSubTypesVariable);
        $method->statementBlock()->add(new ReturnStatement($sprintfCall));

        return $method;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateTraversableSubTypesMethod()
    {
        $this->typeCheck->generateTraversableSubTypesMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $maxIterationsIdentifier = new Identifier('maxIterations');
        $maxIterationsVariable = new Variable($maxIterationsIdentifier);
        $keyTypesVariable = new Variable(new Identifier('keyTypes'));
        $valueTypesVariable = new Variable(new Identifier('valueTypes'));
        $iVariable = new Variable(new Identifier('i'));
        $keyIdentifier = new Identifier('key');
        $keyVariable = new Variable($keyIdentifier);
        $subValueIdentifier = new Identifier('subValue');
        $subValueVariable = new Variable($subValueIdentifier);
        $thisVariable = new Variable(new Identifier('this'));
        $thisTypeMember = new Member(
            $thisVariable,
            new Constant(new Identifier('type'))
        );
        $sortStringConstant = new Constant(new Identifier('SORT_STRING'));

        $method = new ConcreteMethod(
            new Identifier('traversableSubTypes'),
            AccessModifier::PROTECTED_()
        );
        $method->addParameter(new Parameter($valueIdentifier));
        $method->addParameter(new Parameter($maxIterationsIdentifier));

        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $keyTypesVariable,
            new ArrayLiteral
        )));
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $valueTypesVariable,
            new ArrayLiteral
        )));
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $iVariable,
            new Literal(0)
        )));

        $loop = new ForeachStatement(
            $valueVariable,
            new Parameter($keyIdentifier),
            new Parameter($subValueIdentifier)
        );

        $keyTypeArrayPushCall = new Call(
            QualifiedIdentifier::fromString('\array_push')
        );
        $keyTypeArrayPushCall->add($keyTypesVariable);
        $keyTypeCall = new Call($thisTypeMember);
        $keyTypeCall->add($keyVariable);
        $keyTypeArrayPushCall->add($keyTypeCall);
        $loop->statement()->add(new ExpressionStatement($keyTypeArrayPushCall));

        $valueTypeArrayPushCall = new Call(
            QualifiedIdentifier::fromString('\array_push')
        );
        $valueTypeArrayPushCall->add($valueTypesVariable);
        $valueTypeCall = new Call($thisTypeMember);
        $valueTypeCall->add($subValueVariable);
        $valueTypeArrayPushCall->add($valueTypeCall);
        $loop->statement()->add(new ExpressionStatement($valueTypeArrayPushCall));

        $loop->statement()->add(new ExpressionStatement(
            new PostfixIncrement($iVariable)
        ));
        $maxIterationsIf = new IfStatement(new GreaterEqual(
            $iVariable,
            $maxIterationsVariable
        ));
        $maxIterationsIf->trueBranch()->add(new BreakStatement);
        $loop->statement()->add($maxIterationsIf);

        $method->statementBlock()->add($loop);

        $countValueTypesCall = new Call(
            QualifiedIdentifier::fromString('\count')
        );
        $countValueTypesCall->add($valueTypesVariable);
        $noValuesIf = new IfStatement(new Less(
            $countValueTypesCall,
            new Literal(1)
        ));
        $noValuesIf->trueBranch()->add(new ReturnStatement(new Literal('')));
        $method->statementBlock()->add($noValuesIf);

        $keyTypesUniqueCall = new Call(
            QualifiedIdentifier::fromString('\array_unique')
        );
        $keyTypesUniqueCall->add($keyTypesVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $keyTypesVariable,
            $keyTypesUniqueCall
        )));
        $keyTypesSortCall = new Call(
            QualifiedIdentifier::fromString('\sort')
        );
        $keyTypesSortCall->add($keyTypesVariable);
        $keyTypesSortCall->add($sortStringConstant);
        $method->statementBlock()->add(new ExpressionStatement(
            $keyTypesSortCall
        ));

        $valueTypesUniqueCall = new Call(
            QualifiedIdentifier::fromString('\array_unique')
        );
        $valueTypesUniqueCall->add($valueTypesVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $valueTypesVariable,
            $valueTypesUniqueCall
        )));
        $valueTypesSortCall = new Call(
            QualifiedIdentifier::fromString('\sort')
        );
        $valueTypesSortCall->add($valueTypesVariable);
        $valueTypesSortCall->add($sortStringConstant);
        $method->statementBlock()->add(new ExpressionStatement(
            $valueTypesSortCall
        ));

        $keyTypesImplodeCall = new Call(
            QualifiedIdentifier::fromString('\implode')
        );
        $keyTypesImplodeCall->add(new Literal('|'));
        $keyTypesImplodeCall->add($keyTypesVariable);
        $valueTypesImplodeCall = new Call(
            QualifiedIdentifier::fromString('\implode')
        );
        $valueTypesImplodeCall->add(new Literal('|'));
        $valueTypesImplodeCall->add($valueTypesVariable);
        $sprintfCall = new Call(
            QualifiedIdentifier::fromString('\sprintf')
        );
        $sprintfCall->add(new Literal('<%s, %s>'));
        $sprintfCall->add($keyTypesImplodeCall);
        $sprintfCall->add($valueTypesImplodeCall);
        $method->statementBlock()->add(new ReturnStatement(
            $sprintfCall
        ));

        return $method;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateResourceTypeMethod()
    {
        $this->typeCheck->generateResourceTypeMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $ofTypeVariable = new Variable(new Identifier('ofType'));

        $method = new ConcreteMethod(
            new Identifier('resourceType'),
            AccessModifier::PROTECTED_()
        );
        $method->addParameter(new Parameter($valueIdentifier));

        $getResourceTypeCall = new Call(
            QualifiedIdentifier::fromString('\get_resource_type')
        );
        $getResourceTypeCall->add($valueVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $ofTypeVariable,
            $getResourceTypeCall
        )));

        $ofTypeStreamIf = new IfStatement(new StrictEquals(
            new Literal('stream'),
            $ofTypeVariable
        ));
        $streamTypeCall = new Call(new Member(
            new Variable(new Identifier('this')),
            new Constant(new Identifier('streamType'))
        ));
        $streamTypeCall->add($valueVariable);
        $ofTypeStreamIf->trueBranch()->add(new ReturnStatement(
            $streamTypeCall
        ));
        $method->statementBlock()->add($ofTypeStreamIf);

        $sprintfCall = new Call(
            QualifiedIdentifier::fromString('\sprintf')
        );
        $sprintfCall->add(new Literal('resource {ofType: %s}'));
        $sprintfCall->add($ofTypeVariable);
        $method->statementBlock()->add(new ReturnStatement(
            $sprintfCall
        ));

        return $method;
    }

    /**
     * @return ConcreteMethod
     */
    protected function generateStreamTypeMethod()
    {
        $this->typeCheck->generateStreamTypeMethod(func_get_args());

        $valueIdentifier = new Identifier('value');
        $valueVariable = new Variable($valueIdentifier);
        $metaDataVariable = new Variable(new Identifier('metaData'));
        $metaDataModeSubscript = new Subscript(
            $metaDataVariable,
            new Literal('mode')
        );
        $readableVariable = new Variable(new Identifier('readable'));
        $writableVariable = new Variable(new Identifier('writable'));

        $method = new ConcreteMethod(
            new Identifier('streamType'),
            AccessModifier::PROTECTED_()
        );
        $method->addParameter(new Parameter($valueIdentifier));

        $streamGetMetaDataCall = new Call(
            QualifiedIdentifier::fromString('\stream_get_meta_data')
        );
        $streamGetMetaDataCall->add($valueVariable);
        $method->statementBlock()->add(new ExpressionStatement(new Assign(
            $metaDataVariable,
            $streamGetMetaDataCall
        )));

        $readablePregMatchCall = new Call(
            QualifiedIdentifier::fromString('\preg_match')
        );
        $readablePregMatchCall->add(new Literal('/[r+]/'));
        $readablePregMatchCall->add($metaDataModeSubscript);
        $readableIf = new IfStatement(
            $readablePregMatchCall,
            new StatementBlock,
            new StatementBlock
        );
        $readableIf->trueBranch()->add(new ExpressionStatement(new Assign(
            $readableVariable,
            new Literal('true')
        )));
        $readableIf->falseBranch()->add(new ExpressionStatement(new Assign(
            $readableVariable,
            new Literal('false')
        )));
        $method->statementBlock()->add($readableIf);

        $writablePregMatchCall = new Call(
            QualifiedIdentifier::fromString('\preg_match')
        );
        $writablePregMatchCall->add(new Literal('/[waxc+]/'));
        $writablePregMatchCall->add($metaDataModeSubscript);
        $writableIf = new IfStatement(
            $writablePregMatchCall,
            new StatementBlock,
            new StatementBlock
        );
        $writableIf->trueBranch()->add(new ExpressionStatement(new Assign(
            $writableVariable,
            new Literal('true')
        )));
        $writableIf->falseBranch()->add(new ExpressionStatement(new Assign(
            $writableVariable,
            new Literal('false')
        )));
        $method->statementBlock()->add($writableIf);

        $sprintfCall = new Call(
            QualifiedIdentifier::fromString('\sprintf')
        );
        $sprintfCall->add(new Literal('stream {readable: %s, writable: %s}'));
        $sprintfCall->add($readableVariable);
        $sprintfCall->add($writableVariable);
        $method->statementBlock()->add(new ReturnStatement(
            $sprintfCall
        ));

        return $method;
    }

    private $renderer;
    private $typeCheck;
}


<?php

/*
 * This file is part of the andreas-weber/php-runner library.
 *
 * (c) Andreas Weber <code@andreas-weber.me>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace AndreasWeber\Runner;

use AndreasWeber\Runner\Event\Event;
use AndreasWeber\Runner\Exception\LogicException;
use AndreasWeber\Runner\Payload\PayloadInterface;
use AndreasWeber\Runner\Runner\Exception\RunFailedException;
use AndreasWeber\Runner\Task\Collection;
use AndreasWeber\Runner\Task\Exception\FailException;
use AndreasWeber\Runner\Task\Exception\RetryException;
use AndreasWeber\Runner\Task\Exception\SkipException;
use AndreasWeber\Runner\Task\TaskInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;

class Runner implements LoggerAwareInterface
{
    /**
     * @var Collection Task collection
     */
    private $taskCollection;

    /**
     * @var LoggerInterface Logger
     */
    private $logger;

    /**
     * @var \SplObjectStorage Attached runners
     */
    private $runners;

    /**
     * @var EventDispatcher Event dispatcher
     */
    private $eventDispatcher;

    /**
     * __construct()
     *
     * @param Collection $taskCollection
     */
    public function __construct(Collection $taskCollection = null)
    {
        $this->taskCollection = $taskCollection;
        $this->logger = new NullLogger();
        $this->runners = new \SplObjectStorage();
        $this->eventDispatcher = new EventDispatcher();
    }

    /**
     * Sets the task collection.
     *
     * @param Collection $taskCollection
     *
     * @return $this
     */
    public function setTaskCollection(Collection $taskCollection)
    {
        $this->taskCollection = $taskCollection;

        return $this;
    }

    /**
     * Gets the task collection.
     *
     * @return Collection
     */
    public function getTaskCollection()
    {
        if (!$this->taskCollection) {
            throw new LogicException('Can\'t get task collection. None set.');
        }

        return $this->taskCollection;
    }

    /**
     * Invokes the task collection execution.
     * The payload will be passed from task to task.
     *
     * @param PayloadInterface $payload
     *
     * @return PayloadInterface
     */
    public function run(PayloadInterface $payload)
    {
        $tasks = $this->getTaskCollection()->getTasks();
        $tasksCount = $tasks->count();

        if (0 === $tasksCount) {
            throw new LogicException('Can\'t invoke task run. Empty task collection set.');
        }

        $this->log(LogLevel::INFO, sprintf('Starting runner with %s tasks ready for execution.', $tasksCount));
        $this->dispatch('runner.start', null, $payload);

        foreach ($tasks as $task) {
            try {
                /** @var TaskInterface $task */
                $task->setPayload($payload);
                $this->runTask($task, $payload);
            } catch (\Exception $e) {
                $this->logTask(
                    $task,
                    LogLevel::ERROR,
                    sprintf('An exception was thrown. Message: %s', $e->getMessage())
                );
                $this->dispatch('runner.failure', null, null, null, $e);
                throw new RunFailedException(
                    'Complete run failed: ' . $e->getMessage(),
                    $e->getCode(),
                    $e
                );
            }
        }

        $this->log(LogLevel::INFO, 'All tasks were processed.');
        $this->log(LogLevel::INFO, 'Calling attached runners.');
        $this->notify($payload);

        $this->log(LogLevel::INFO, 'Execution successful.');
        $this->dispatch('runner.success', null, $payload);

        return $payload;
    }

    /**
     * Invokes the task execution.
     *
     * @param TaskInterface    $task
     * @param PayloadInterface $payload
     *
     * @return null
     */
    private function runTask(TaskInterface $task, PayloadInterface $payload)
    {
        $this->logTask($task, LogLevel::INFO, 'Starting execution.');

        try {
            if (!$task->unless()) {
                $this->dispatch('runner.task.unless', $task, $payload);
                $this->logTask($task, LogLevel::INFO, 'Skipping because unless() returned boolean false.');
                return;
            }

            $this->dispatch('runner.task.start', $task, $payload);

            $task->setUp();
            $exitCode = (int)$task->run($payload) ?: 0;
            $task->tearDown();

            if ($task->isFailOnError() && $exitCode !== 0) {
                throw new FailException(
                    sprintf(
                        'Task: %s failed with exit code %s',
                        get_class($task),
                        $exitCode
                    )
                );
            }

            $message = sprintf('Task exited with status code %s', $exitCode);
            if ($exitCode === 0) {
                $this->logTask($task, LogLevel::INFO, $message);
            } else {
                $this->logTask($task, LogLevel::WARNING, $message);
            }

            $this->dispatch('runner.task.success', $task, $payload, $exitCode);
            $task->markAsSuccessfullyExecuted();
        } catch (SkipException $e) {
            $this->logTask($task, LogLevel::INFO, 'Skipping.');
            $this->dispatch('runner.task.skip', $task, $payload);
        } catch (RetryException $e) {
            $this->logTask($task, LogLevel::NOTICE, 'Retry thrown. Starting again.');
            $this->dispatch('runner.task.retry', $task, $payload);
            if (!$task->getMaxRetries()) {
                throw new LogicException('A retry exception was thrown, but no retries instance was set.');
            }
            $task->getMaxRetries()->increase();
            $this->runTask($task, $payload);
            return;
        } catch (FailException $e) {
            $this->logTask(
                $task,
                LogLevel::WARNING,
                sprintf(
                    'Failure thrown. Given message: %s',
                    $e->getMessage()
                )
            );

            $exitCode = $e->getCode();
            if (is_int($exitCode)) {
                $this->dispatch('runner.task.failure', $task, $payload, $exitCode);
            } else {
                $this->dispatch('runner.task.failure', $task, $payload);
            }

            throw $e;
        }

        $this->logTask($task, LogLevel::INFO, 'Execution successful.');
    }

    /**
     * Sets a logger instance on the object
     *
     * @param LoggerInterface $logger
     *
     * @return $this
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return null
     */
    protected function log($level, $message, array $context = array())
    {
        if ($this->logger) {
            $this->logger->log($level, $message, $context);
        }
    }

    /**
     * Logs with an arbitrary level.
     * Specialized to pass a task instance.
     *
     * @param TaskInterface $task
     * @param mixed         $level
     * @param string        $message
     * @param array         $context
     *
     * @return null
     */
    protected function logTask(TaskInterface $task, $level, $message, array $context = array())
    {
        $class = get_class($task);
        $message = sprintf('Task: %s. ', $class) . $message;

        $this->log($level, $message, $context);
    }

    /**
     * Notifies all attached runners to start execution with passed payload.
     *
     * @param PayloadInterface $payload
     *
     * @return $this
     */
    public function notify(PayloadInterface $payload)
    {
        foreach ($this->runners as $runner) {
            /** @var Runner $runner */
            $runner->run($payload);
        }

        return $this;
    }

    /**
     * Attaches a runner.
     *
     * @param Runner $runner
     *
     * @return $this
     */
    public function attach(Runner $runner)
    {
        if ($this->runners->contains($runner)) {
            throw new LogicException('Can\'t attach already attached runner.');
        }

        $this->runners->attach($runner);

        return $this;
    }

    /**
     * Detaches a runner.
     *
     * @param Runner $runner
     *
     * @return $this
     */
    public function detach(Runner $runner)
    {
        if (!$this->runners->contains($runner)) {
            throw new LogicException('Can\'t detach not attached runner.');
        }

        $this->runners->detach($runner);

        return $this;
    }

    /**
     * Register an callable to an event.
     *
     * @param string   $eventName
     * @param callable $callable
     *
     * @return $this
     */
    public function on($eventName, callable $callable)
    {
        \Assert\that($eventName)->string()->notEmpty();

        $this->eventDispatcher->addListener($eventName, $callable);

        return $this;
    }

    /**
     * Dispatches an event.
     *
     * @param string                $eventName
     * @param null|TaskInterface    $task
     * @param null|PayloadInterface $payload
     * @param null|int              $exitCode
     * @param null|\Exception       $exception
     *
     * @return null
     */
    private function dispatch(
        $eventName,
        TaskInterface $task = null,
        PayloadInterface $payload = null,
        $exitCode = null,
        \Exception $exception = null
    )
    {
        \Assert\that($eventName)->string()->notEmpty();

        $event = new Event();
        $event->setRunner($this);

        if ($task) {
            $event->setTask($task);
        }

        if ($payload) {
            $event->setPayload($payload);
        }

        if (!is_null($exitCode)) {
            $event->setExitCode($exitCode);
        }

        if ($exception) {
            $event->setException($exception);
        }

        $this->logger->debug(
            sprintf(
                "Dispatching event '%s'",
                $eventName
            )
        );

        $this->eventDispatcher->dispatch($eventName, $event);
    }
}


<?php namespace uMailPHP;

/**
 * Mail class.
 *
 * Creates and sends emails.
 *
 * This class supports attachments and custom headers.
 *
 * This class is based on a class by Alejandro Gervasio
 * http://www.devshed.com/cp/bio/Alejandro-Gervasio/
 *
 * @license https://www.apache.org/licenses/LICENSE-2.0
 * @author Hunter Perrin <hperrin@gmail.com>
 * @copyright SciActive.com
 * @link http://umailphp.org/
 */
class Mail {
  const VERSION = '2.4.0';
  /**
   * A copy of the uMailPHP config.
   *
   * @var array
   * @access public
   */
  public static $config;
  /**
   * The sender's email address.
   *
   * @var string
   */
  public $sender;
  /**
   * The recipient's email address.
   *
   * @var string
   */
  public $recipient;
  /**
   * The message subject.
   *
   * @var string
   */
  public $subject;
  /**
   * The message text's MIME type.
   *
   * @var string
   */
  public $textMimeType = 'text/html';
  /**
   * An array of headers to include in the message.
   *
   * @var array
   */
  public $headers = [];
  /**
   * An array of known MIME types.
   *
   * The values are the mime types, and the keys are the file extensions.
   *
   * @var array
   */
  public $mimeTypes = [];
  /**
   * An array of attachments.
   *
   * @var array
   */
  public $attachments = [];

  /**
   * Apply configuration to uMailPHP.
   *
   * $config should be an associative array of uMailPHP configuration. Use the
   * following form:
   *
   * [
   *     'testing_mode' => false,
   *     'site_link' => 'http://example.com/'
   * ]
   *
   * @param array $config An associative array of uMailPHP's configuration.
   */
  public static function configure($config = []) {
    $defaults = include dirname(__DIR__).'/conf/defaults.php';
    self::$config = array_replace($defaults, $config);
  }

  /**
   * @param string The name of the mail definition class.
   * @param mixed $recipient The recipient's email address, or a recipient object. If left null, the rendition must have a recipient.
   * @param array $macros An associative array of macros. These override macros from the definition.
   * @param string|null $sender The sender's email address. If left null, the rendition or default sender will be used.
   * @param \uMailPHP\Entities\Rendition An optional rendition. If left null, the last modified, enabled rendition will be used. If false, no rendition will be used.
   * @param \uMailPHP\Entities\Template An optional template. If left null, the last modified, enabled template will be used. If false, the default template will be used.
   */
  public function __construct($definition, $recipient = null, $macros = [], $sender = null, $rendition = null, $template = null) {
    if (!class_exists($definition) || !is_subclass_of($definition, '\uMailPHP\Definition')) {
      throw new \InvalidArgumentException('Mail definition is required.');
    }

    // Format recipient.
    if ($recipient && is_string($recipient)) {
      $recipient = (object) ['email' => $recipient];
    }

    // Find any renditions.
    if ($rendition === null) {
      $rendition = \Nymph\Nymph::getEntity(
          [
            'class' => '\uMailPHP\Entities\Rendition',
            'sort' => 'mdate',
            'reverse' => true
          ],
          ['&',
            'strict' => [
              ['enabled', true],
              ['definition', $definition]
            ]
          ]
      );
    }

    // Get the email sender.
    if ($rendition && !isset($sender) && !empty($rendition->from)) {
      $sender = $rendition->from;
    }

    // Get the email recipient(s).
    if (!$recipient) {
      // If it's supposed to have a recipient already, report failure.
      if ($definition::$expectsRecipient) {
        throw new \UnexpectedValueException('This email definition requires a recipient.');
      }
      if ($rendition) {
        if ($rendition->to) {
          // Check Tilmeld users/groups if Tilmeld is loaded.
          if (class_exists('\Tilmeld\User') && strpos($rendition->to, ',') === false) {
            if (preg_match('/<.+@.+>/', $rendition->to)) {
              $check_email = trim(preg_replace('/^.*<(.+@.+)>.*$/', '$1', $rendition->to));
            } else {
              $check_email = trim($rendition->to);
            }
            // Check for a user or group with that email.
            $user = \Nymph\Nymph::getEntity(
                ['class' => '\Tilmeld\User'],
                ['&',
                  'strict' => ['email', $check_email]
                ]
            );
            if ($user) {
              $recipient = $user;
            } else {
              $group = \Nymph\Nymph::getEntity(
                  ['class' => '\Tilmeld\Group'],
                  ['&',
                    'strict' => ['email', $check_email]
                  ]
              );
              if ($group) {
                $recipient = $group;
              }
            }
          }
          if (!$recipient) {
            $recipient = (object) ['email' => $rendition->to];
          }
        }
      } else {
        // Send to the master address if there's no recipient.
        if (!Mail::$config['master_address']) {
          throw new \UnexpectedValueException('This email needs a recipient and no master address is set.');
        }
        $recipient = (object) ['email' => Mail::$config['master_address']];
      }
    }

    // Get the email contents.
    $body = [];
    if ($rendition) {
      $body['subject'] = $rendition->subject;
      $body['content'] = $rendition->content;
    } else {
      $body['subject'] = $definition::getSubject();
      $body['content'] = $definition::getHTML();
    }

    // Get the template.
    if ($template === null) {
      $template = \Nymph\Nymph::getEntity(
          [
            'class' => '\uMailPHP\Entities\Template',
            'sort' => 'mdate',
            'reverse' => true
          ],
          ['&',
            'strict' => ['enabled', true]
          ]
      );
    }
    // If there is no template, use a default one.
    if (!$template) {
      $template = new Entities\Template();
      $template->defaultContent();
    }

    // Build the body of the email.
    $body['content'] = str_replace(
        '#content#',
        $body['content'],
        str_replace(
            '#content#',
            $template->content,
            $template->document
        )
    );

    // Replace macros and search strings.
    foreach ($body as &$cur_field) {
      // Some of these str_replace calls are wrapped in a strpos call,
      // because they involve some processing just to make the call. In a
      // system sending out millions of emails, that adds up to a lot of
      // wasted CPU.
      foreach ((array) $template->replacements as $cur_string) {
        if (!$cur_string['macros']) {
          continue;
        }
        $cur_field = str_replace($cur_string['search'], $cur_string['replace'], $cur_field);
      }
      if (strpos($cur_field, '#subject#') !== false) {
        $cur_field = str_replace('#subject#', htmlspecialchars($body['subject']), $cur_field);
      }
      // Links
      if (strpos($cur_field, '#site_link#') !== false) {
        $cur_field = str_replace('#site_link#', htmlspecialchars(Mail::$config['site_link']), $cur_field);
      }
      // Recipient
      if (strpos($cur_field, '#to_username#') !== false) {
        $cur_field = str_replace('#to_username#', htmlspecialchars(isset($recipient->username) ? $recipient->username : (isset($recipient->groupname) ? $recipient->groupname : '')), $cur_field);
      }
      if (strpos($cur_field, '#to_name#') !== false) {
        $cur_field = str_replace('#to_name#', htmlspecialchars(isset($recipient->name) ? $recipient->name : ''), $cur_field);
      }
      if (strpos($cur_field, '#to_first_name#') !== false) {
        $cur_field = str_replace('#to_first_name#', htmlspecialchars(isset($recipient->nameFirst) ? $recipient->nameFirst : ''), $cur_field);
      }
      if (strpos($cur_field, '#to_last_name#') !== false) {
        $cur_field = str_replace('#to_last_name#', htmlspecialchars(isset($recipient->nameLast) ? $recipient->nameLast : ''), $cur_field);
      }
      if (strpos($cur_field, '#to_email#') !== false) {
        $cur_field = str_replace('#to_email#', htmlspecialchars(isset($recipient->email) ? $recipient->email : ''), $cur_field);
      }
      // Current User with Tilmeld.
      if (class_exists('\Tilmeld\User') && \Tilmeld\User::current()) {
        if (strpos($cur_field, '#username#') !== false) {
          $cur_field = str_replace('#username#', htmlspecialchars(\Tilmeld\User::current()->username), $cur_field);
        }
        if (strpos($cur_field, '#name#') !== false) {
          $cur_field = str_replace('#name#', htmlspecialchars(\Tilmeld\User::current()->name), $cur_field);
        }
        if (strpos($cur_field, '#first_name#') !== false) {
          $cur_field = str_replace('#first_name#', htmlspecialchars(\Tilmeld\User::current()->nameFirst), $cur_field);
        }
        if (strpos($cur_field, '#last_name#') !== false) {
          $cur_field = str_replace('#last_name#', htmlspecialchars(\Tilmeld\User::current()->nameLast), $cur_field);
        }
        if (strpos($cur_field, '#email#') !== false) {
          $cur_field = str_replace('#email#', htmlspecialchars(\Tilmeld\User::current()->email), $cur_field);
        }
      }
      // Date/Time
      if (strpos($cur_field, '#datetime_sort#') !== false) {
        $cur_field = str_replace('#datetime_sort#', htmlspecialchars(Mail::formatDate(time(), 'full_sort')), $cur_field);
      }
      if (strpos($cur_field, '#datetime_short#') !== false) {
        $cur_field = str_replace('#datetime_short#', htmlspecialchars(Mail::formatDate(time(), 'full_short')), $cur_field);
      }
      if (strpos($cur_field, '#datetime_med#') !== false) {
        $cur_field = str_replace('#datetime_med#', htmlspecialchars(Mail::formatDate(time(), 'full_med')), $cur_field);
      }
      if (strpos($cur_field, '#datetime_long#') !== false) {
        $cur_field = str_replace('#datetime_long#', htmlspecialchars(Mail::formatDate(time(), 'full_long')), $cur_field);
      }
      if (strpos($cur_field, '#date_sort#') !== false) {
        $cur_field = str_replace('#date_sort#', htmlspecialchars(Mail::formatDate(time(), 'date_sort')), $cur_field);
      }
      if (strpos($cur_field, '#date_short#') !== false) {
        $cur_field = str_replace('#date_short#', htmlspecialchars(Mail::formatDate(time(), 'date_short')), $cur_field);
      }
      if (strpos($cur_field, '#date_med#') !== false) {
        $cur_field = str_replace('#date_med#', htmlspecialchars(Mail::formatDate(time(), 'date_med')), $cur_field);
      }
      if (strpos($cur_field, '#date_long#') !== false) {
        $cur_field = str_replace('#date_long#', htmlspecialchars(Mail::formatDate(time(), 'date_long')), $cur_field);
      }
      if (strpos($cur_field, '#time_sort#') !== false) {
        $cur_field = str_replace('#time_sort#', htmlspecialchars(Mail::formatDate(time(), 'time_sort')), $cur_field);
      }
      if (strpos($cur_field, '#time_short#') !== false) {
        $cur_field = str_replace('#time_short#', htmlspecialchars(Mail::formatDate(time(), 'time_short')), $cur_field);
      }
      if (strpos($cur_field, '#time_med#') !== false) {
        $cur_field = str_replace('#time_med#', htmlspecialchars(Mail::formatDate(time(), 'time_med')), $cur_field);
      }
      if (strpos($cur_field, '#time_long#') !== false) {
        $cur_field = str_replace('#time_long#', htmlspecialchars(Mail::formatDate(time(), 'time_long')), $cur_field);
      }
      // System
      if (strpos($cur_field, '#site_name#') !== false) {
        $cur_field = str_replace('#site_name#', htmlspecialchars(Mail::$config['site_name']), $cur_field);
      }
      // Argument Macros
      foreach ($macros as $cur_name => $cur_value) {
        $cur_field = str_replace("#$cur_name#", $cur_value, $cur_field);
      }
      // Definition Macros
      foreach ($definition::$macros as $cur_name => $cur_desc) {
        if (strpos($cur_field, "#$cur_name#") !== false) {
          $cur_field = str_replace("#$cur_name#", $definition::getMacro($cur_name), $cur_field);
        }
      }
      foreach ((array) $template->replacements as $cur_string) {
        if ($cur_string['macros']) {
          continue;
        }
        $cur_field = str_replace($cur_string['search'], $cur_string['replace'], $cur_field);
      }
    }
    unset($cur_field);

    // Add additional recipients.
    if ($rendition) {
      if ($rendition->cc) {
        $email->addHeader('CC', $rendition->cc);
      }
      if ($rendition->bcc) {
        $email->addHeader('BCC', $rendition->bcc);
      }
    }

    // Get default values for missing parameters.
    if (!isset($sender)) {
      $sender = Mail::$config['from_address'];
    }
    $destination = isset($recipient->name) ? "\"".str_replace('"', '', $recipient->name)."\" <{$recipient->email}>" : (isset($recipient->email) ? $recipient->email : '');

    // Validate incoming parameters.
    if (!preg_match('/^.+@.+$/', $sender)) {
      throw new \InvalidArgumentException('Invalid value for email sender.');
    }
    if (!preg_match('/^.+@.+$/', $destination)) {
      throw new \InvalidArgumentException('Invalid value for email recipient.');
    }
    if (!isset($body['subject']) || !is_string($body['subject']) || strlen($body['subject']) > 255) {
      throw new \LengthException('Invalid length for email subject.');
    }
    if (!isset($body['content']) || !is_string($body['content']) || strlen($body['content']) < 1) {
      throw new \UnexpectedValueException('Invalid value for email message.');
    }

    // Assign the complete variables.
    $this->sender = $sender;
    $this->recipient = $destination;
    $this->subject = $body['subject'];
    $this->message = $body['content'];
    // Define some default MIME headers
    $this->headers['MIME-Version'] = '1.0';
    $this->headers['Content-Type'] = 'multipart/mixed;boundary="MIME_BOUNDRY"';
    //$this->headers['X-Mailer'] = 'PHP5';
    $this->headers['X-Priority'] = '3';
    $this->headers['User-Agent'] = 'uMailPHP '.Mail::VERSION;
    // Define some default MIME types
    $this->mimeTypes['doc'] = 'application/msword';
    $this->mimeTypes['pdf'] = 'application/pdf';
    $this->mimeTypes['gz'] = 'application/x-gzip';
    $this->mimeTypes['exe'] = 'application/x-msdos-program';
    $this->mimeTypes['rar'] = 'application/x-rar-compressed';
    $this->mimeTypes['swf'] = 'application/x-shockwave-flash';
    $this->mimeTypes['tgz'] = 'application/x-tar-gz';
    $this->mimeTypes['tar'] = 'application/x-tar';
    $this->mimeTypes['zip'] = 'application/zip';
    $this->mimeTypes['mid'] = 'audio/midi';
    $this->mimeTypes['mp3'] = 'audio/mpeg';
    $this->mimeTypes['au'] = 'audio/ulaw';
    $this->mimeTypes['aif'] = 'audio/x-aiff';
    $this->mimeTypes['aiff'] = 'audio/x-aiff';
    $this->mimeTypes['wma'] = 'audio/x-ms-wma';
    $this->mimeTypes['wav'] = 'audio/x-wav';
    $this->mimeTypes['gif'] = 'image/gif';
    $this->mimeTypes['jpg'] = 'image/jpeg';
    $this->mimeTypes['jpeg'] = 'image/jpeg';
    $this->mimeTypes['jpe'] = 'image/jpeg';
    $this->mimeTypes['png'] = 'image/png';
    $this->mimeTypes['tif'] = 'image/tiff';
    $this->mimeTypes['tiff'] = 'image/tiff';
    $this->mimeTypes['css'] = 'text/css';
    $this->mimeTypes['htm'] = 'text/html';
    $this->mimeTypes['html'] = 'text/html';
    $this->mimeTypes['txt'] = 'text/plain';
    $this->mimeTypes['rtf'] = 'text/rtf';
    $this->mimeTypes['xml'] = 'text/xml';
    $this->mimeTypes['flv'] = 'video/flv';
    $this->mimeTypes['mpe'] = 'video/mpeg';
    $this->mimeTypes['mpeg'] = 'video/mpeg';
    $this->mimeTypes['mpg'] = 'video/mpeg';
    $this->mimeTypes['mov'] = 'video/quicktime';
    $this->mimeTypes['asf'] = 'video/x-ms-asf';
    $this->mimeTypes['wmv'] = 'video/x-ms-wmv';
    $this->mimeTypes['avi'] = 'video/x-msvideo';
  }

  /**
   * Create text part of the message.
   *
   * @access private
   * @return string The text.
   */
  private function buildTextPart() {
    return "--MIME_BOUNDRY\nContent-Type: {$this->textMimeType}; charset=utf-8\nContent-Transfer-Encoding: 7bit\n\n\n{$this->message}\n\n";
  }

  /**
   * Create attachments part of the message.
   *
   * @access private
   * @return string The attachment section.
   */
  private function buildAttachmentPart() {
    if (count($this->attachments) > 0) {
      $attachment_part = '';
      foreach ($this->attachments as $attachment) {
        $file_str = chunk_split(base64_encode(file_get_contents($attachment)));
        $attachment_part .= "--MIME_BOUNDRY\nContent-Type: ".$this->getMimeType($attachment)."; name=".basename($attachment)."\nContent-disposition: attachment\nContent-Transfer-Encoding: base64\n\n{$file_str}\n\n";
      }
      return $attachment_part;
    }
  }

  /**
   * Create message headers.
   *
   * @access private
   * @param array $required_headers Any headers that should append/replace the defined headers.
   * @return string The headers.
   */
  private function buildHeaders($required_headers = []) {
    $build_headers = array_merge($this->headers, $required_headers);
    $headers = [];
    foreach ($build_headers as $name => $value) {
      $headers[] = "{$name}: {$value}";
    }
    return implode("\r\n", $headers)."\nThis is a multi-part message in MIME format.\n";
  }

  /**
   * Add new header.
   *
   * @param string $name The header's name.
   * @param string $value The header's value.
   */
  public function addHeader($name, $value) {
    $this->headers[$name] = $value;
  }

  /**
   * Add new attachment.
   *
   * @param string $attachment The attachment filename.
   * @return bool True on success, false on failure.
   */
  public function addAttachment($attachment) {
    if (!file_exists($attachment)) {
      pines_error('Invalid attachment.');
      return false;
    }
    $this->attachments[] = $attachment;
    return true;
  }

  /**
   * Get MIME Type of attachment.
   *
   * @param string $attachment The attachment filename.
   * @return mixed MIME type on success, null on failure.
   */
  public function getMimeType($attachment) {
    $attachment = explode('.', basename($attachment));
    if (!isset($this->mimeTypes[strtolower($attachment[count($attachment) - 1])])) {
      pines_error('MIME Type not found.');
      return null;
    }
    return $this->mimeTypes[strtolower($attachment[count($attachment) - 1])];
  }

  /**
   * Send email.
   *
   * @return bool True on success, false on failure.
   */
  public function send() {
    // First verify values.
    if (!preg_match('/^.+@.+$/', $this->sender)) {
      return false;
    }
    if (!preg_match('/^.+@.+$/', $this->recipient)) {
      return false;
    }
    if (!$this->subject || strlen($this->subject) > 255) {
      return false;
    }

    // Headers that must be in the sent message.
    $required_headers = [];

    // Are we in testing mode?
    if (Mail::$config['testing_mode']) {
      // If the testing email is empty, just return true.
      if (empty(Mail::$config['testing_email'])) {
        return true;
      }
      // The testing email isn't empty, so replace stuff now.
      // Save the original to, cc, and bcc in additional headers.
      $required_headers['X-Testing-Original-To'] = $this->recipient;
      foreach ($this->headers as $name => $value) {
        switch (strtolower($name)) {
          case 'cc':
            $this->headers['X-Testing-Original-Cc'] = $value;
            $required_headers[$name] = '';
            break;
          case 'bcc':
            $this->headers['X-Testing-Original-Bcc'] = $value;
            $required_headers[$name] = '';
            break;
        }
      }
      $to = Mail::$config['testing_email'];
      $subject = '*Test* '.$this->subject;
    } else {
      $to = $this->recipient;
      $subject = $this->subject;
    }
    // Add from headers.
    $required_headers['From'] = $this->sender;
    $required_headers['Return-Path'] = $this->sender;
    $required_headers['Reply-To'] = $this->sender;
    $required_headers['X-Sender'] = $this->sender;
    $headers = $this->buildHeaders($required_headers);
    $message = $this->buildTextPart().$this->buildAttachmentPart()."--MIME_BOUNDRY--\n";

    // Now send the mail.
    return mail($to, $subject, $message, $headers, Mail::$config['additional_parameters']);
  }

  /**
   * Format a date using the DateTime class.
   *
   * $type can be any of the following:
   *
   * - full_sort - Date and time, big endian and 24 hour format so it is sortable.
   * - full_long - Date and time, long format.
   * - full_med - Date and time, medium format.
   * - full_short - Date and time, short format.
   * - date_sort - Only the date, big endian so it is sortable.
   * - date_long - Only the date, long format.
   * - date_med - Only the date, medium format.
   * - date_short - Only the date, short format.
   * - time_sort - Only the time, 24 hour format so it is sortable.
   * - time_long - Only the time, long format.
   * - time_med - Only the time, medium format.
   * - time_short - Only the time, short format.
   * - custom - Use whatever is passed in $format.
   *
   * @param int $timestamp The timestamp to format.
   * @param string $type The type of formatting to use.
   * @param string $format The format to use if type is 'custom'.
   * @param DateTimeZone|string|null $timezone The timezone to use for formatting. Defaults to date_default_timezone_get().
   * @return string The formatted date.
   */
  public static function formatDate($timestamp, $type = 'full_sort', $format = '', $timezone = null) {
    // Determine the format to use.
    switch ($type) {
      case 'date_sort':
        $format = 'Y-m-d';
        break;
      case 'date_long':
        $format = 'l, F j, Y';
        break;
      case 'date_med':
        $format = 'j M Y';
        break;
      case 'date_short':
        $format = 'n/d/Y';
        break;
      case 'time_sort':
        $format = 'H:i T';
        break;
      case 'time_long':
        $format = 'g:i:s A T';
        break;
      case 'time_med':
        $format = 'g:i:s A';
        break;
      case 'time_short':
        $format = 'g:i A';
        break;
      case 'full_sort':
        $format = 'Y-m-d H:i T';
        break;
      case 'full_long':
        $format = 'l, F j, Y g:i A T';
        break;
      case 'full_med':
        $format = 'j M Y g:i A T';
        break;
      case 'full_short':
        $format = 'n/d/Y g:i A T';
        break;
      case 'custom':
      default:
        break;
    }
    // Create a date object from the timestamp.
    try {
      $date = new \DateTime(gmdate('c', (int) $timestamp));
      if (isset($timezone)) {
        if (!is_object($timezone)) {
          $timezone = new \DateTimeZone($timezone);
        }
        $date->setTimezone($timezone);
      } else {
        $date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
      }
    } catch (\Exception $e) {
      throw new \Exception("Error formatting date: $e");
    }
    return $date->format($format);
  }

  /**
   * Format a date range into a human understandable phrase.
   *
   * $format is built using macros, which are substrings replaced by the
   * corresponding number of units. There are singular macros, such as #year#,
   * which are used if the number of that unit is 1. For example, if the range
   * is 1 year and both #year# and #years# are present, #year# will be used
   * and #years# will be ignored. This allows you to use a different
   * description for each one. You accomplish this by surrounding the macro
   * and its description in curly brackets. If the unit is 0, everything in
   * that curly bracket will be removed. This allows you to place both #year#
   * and #years# and always end up with the right one.
   *
   * Since the units in curly brackets that equal 0 are removed, you can
   * include as many as you want and only the relevant ones will be used. If
   * you choose not to include one, such as year, then the next available one
   * will include the time that would have been placed in it. For example, if
   * the time range is 2 years, but you only include months, then months will
   * be set to 24.
   *
   * After formatting, any leading and trailing whitespace is trimmed before
   * the result is returned.
   *
   * $format can contain the following macros:
   *
   * - #years# - The number of years.
   * - #year# - The number 1 if applicable.
   * - #months# - The number of months.
   * - #month# - The number 1 if applicable.
   * - #weeks# - The number of weeks.
   * - #week# - The number 1 if applicable.
   * - #days# - The number of days.
   * - #day# - The number 1 if applicable.
   * - #hours# - The number of hours.
   * - #hour# - The number 1 if applicable.
   * - #minutes# - The number of minutes.
   * - #minute# - The number 1 if applicable.
   * - #seconds# - The number of seconds.
   * - #second# - The number 1 if applicable.
   *
   * If $format is left null, it defaults to the following:
   *
   * "{#years# years}{#year# year} {#months# months}{#month# month} {#days# days}{#day# day} {#hours# hours}{#hour# hour} {#minutes# minutes}{#minute# minute} {#seconds# seconds}{#second# second}"
   *
   * Here are some examples of formats and what would be outputted given a
   * time range of 2 years 5 months 1 day and 4 hours. (These values were
   * calculated on Fri Oct 14 2011 in San Diego, which has DST. 2012 is a leap
   * year.)
   *
   * - "#years# years {#days# days}{#day# day}" - 2 years 152 days
   * - "{#months# months}{#month# month} {#days# days}{#day# day}" - 29 months 1 day
   * - "{#weeks# weeks}{#week# week} {#days# days}{#day# day}" - 126 weeks 1 day
   * - "#days# days #hours# hours #minutes# minutes" - 883 days 4 hours 0 minutes
   * - "{#minutes#min} {#seconds#sec}" - 1271760min
   * - "#seconds#" - 76305600
   *
   * @param int $startTimestamp The timestamp of the beginning of the date range.
   * @param int $endTimestamp The timestamp of the end of the date range.
   * @param string $format The format to use. See the function description for details on the format.
   * @param DateTimeZone|string|null $timezone The timezone to use for formatting. Defaults to date_default_timezone_get().
   * @return string The formatted date range.
   */
  public static function formatDateRange($startTimestamp, $endTimestamp, $format = null, $timezone = null) {
    if (!$format) {
      $format = '{#years# years}{#year# year} {#months# months}{#month# month} {#days# days}{#day# day} {#hours# hours}{#hour# hour} {#minutes# minutes}{#minute# minute} {#seconds# seconds}{#second# second}';
    }

    // If it's a negative range, flip the values.
    $negative = ($endTimestamp < $startTimestamp) ? '-' : '';
    if ($negative == '-') {
      $tmp = $endTimestamp;
      $endTimestamp = $startTimestamp;
      $startTimestamp = $tmp;
    }
    // Create a date object from the timestamp.
    try {
      $start_date = new DateTime(gmdate('c', (int) $startTimestamp));
      $end_date = new DateTime(gmdate('c', (int) $endTimestamp));
      if (isset($timezone)) {
        if (!is_object($timezone)) {
          $timezone = new DateTimeZone($timezone);
        }
        $start_date->setTimezone($timezone);
        $end_date->setTimezone($timezone);
      } else {
        $start_date->setTimezone(new DateTimeZone(date_default_timezone_get()));
        $end_date->setTimezone(new DateTimeZone(date_default_timezone_get()));
      }
    } catch (Exception $e) {
      return '';
    }

    if (strpos($format, '#year#') !== false || strpos($format, '#years#') !== false) {
      // Calculate number of years between the two dates.
      $years = (int) $end_date->format('Y') - (int) $start_date->format('Y');
      // Be sure we didn't go too far.
      $test_date = clone $start_date;
      $test_date->modify('+'.$years.' years');
      $test_timestamp = (int) $test_date->format('U');
      if ($test_timestamp > $endTimestamp) {
        $years--;
      }
      if (strpos($format, '#year#') !== false && $years == 1) {
        $format = preg_replace('/\{?([^{}]*)#year#([^{}]*)\}?/s', '${1}'.$negative.$years.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#years#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#years#') !== false) {
        if ($years <> 0) {
          $format = preg_replace('/\{?([^{}]*)#years#([^{}]*)\}?/s', '${1}'.$negative.$years.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#years#([^{}]*)\}/s', '/#years#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#year#([^{}]*)\}/s', '', $format);
      }
      $start_date->modify('+'.$years.' years');
      $startTimestamp = (int) $start_date->format('U');
    }

    if (strpos($format, '#month#') !== false || strpos($format, '#months#') !== false) {
      // Calculate number of months.
      $years = (int) $end_date->format('Y') - (int) $start_date->format('Y');
      $months = ($years * 12) + ((int) $end_date->format('n') - (int) $start_date->format('n'));
      // Be sure we didn't go too far.
      $test_date = clone $start_date;
      $test_date->modify('+'.$months.' months');
      $test_timestamp = (int) $test_date->format('U');
      if ($test_timestamp > $endTimestamp) {
        $months--;
      }
      if (strpos($format, '#month#') !== false && $months == 1) {
        $format = preg_replace('/\{?([^{}]*)#month#([^{}]*)\}?/s', '${1}'.$negative.$months.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#months#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#months#') !== false) {
        if ($months <> 0) {
          $format = preg_replace('/\{?([^{}]*)#months#([^{}]*)\}?/s', '${1}'.$negative.$months.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#months#([^{}]*)\}/s', '/#months#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#month#([^{}]*)\}/s', '', $format);
      }
      $start_date->modify('+'.$months.' months');
      $startTimestamp = (int) $start_date->format('U');
    }

    if (strpos($format, '#week#') !== false || strpos($format, '#weeks#') !== false) {
      // Calculate number of weeks.
      $weeks = floor(($endTimestamp - $startTimestamp) / 604800);
      // Be sure we didn't go too far.
      $test_date = clone $start_date;
      $test_date->modify('+'.$weeks.' weeks');
      $test_timestamp = (int) $test_date->format('U');
      if ($test_timestamp > $endTimestamp) {
        $weeks--;
      }
      if (strpos($format, '#week#') !== false && $weeks == 1) {
        $format = preg_replace('/\{?([^{}]*)#week#([^{}]*)\}?/s', '${1}'.$negative.$weeks.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#weeks#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#weeks#') !== false) {
        if ($weeks <> 0) {
          $format = preg_replace('/\{?([^{}]*)#weeks#([^{}]*)\}?/s', '${1}'.$negative.$weeks.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#weeks#([^{}]*)\}/s', '/#weeks#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#week#([^{}]*)\}/s', '', $format);
      }
      $start_date->modify('+'.$weeks.' weeks');
      $startTimestamp = (int) $start_date->format('U');
    }

    if (strpos($format, '#day#') !== false || strpos($format, '#days#') !== false) {
      // Calculate number of days.
      $days = floor(($endTimestamp - $startTimestamp) / 86400);
      // Be sure we didn't go too far.
      $test_date = clone $start_date;
      $test_date->modify('+'.$days.' days');
      $test_timestamp = (int) $test_date->format('U');
      if ($test_timestamp > $endTimestamp) {
        $days--;
      }
      if (strpos($format, '#day#') !== false && $days == 1) {
        $format = preg_replace('/\{?([^{}]*)#day#([^{}]*)\}?/s', '${1}'.$negative.$days.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#days#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#days#') !== false) {
        if ($days <> 0) {
          $format = preg_replace('/\{?([^{}]*)#days#([^{}]*)\}?/s', '${1}'.$negative.$days.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#days#([^{}]*)\}/s', '/#days#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#day#([^{}]*)\}/s', '', $format);
      }
      $start_date->modify('+'.$days.' days');
      $startTimestamp = (int) $start_date->format('U');
    }

    if (strpos($format, '#hour#') !== false || strpos($format, '#hours#') !== false) {
      // Calculate number of hours.
      $hours = floor(($endTimestamp - $startTimestamp) / 3600);
      // Hours are constant, so we didn't go too far.
      if (strpos($format, '#hour#') !== false && $hours == 1) {
        $format = preg_replace('/\{?([^{}]*)#hour#([^{}]*)\}?/s', '${1}'.$negative.$hours.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#hours#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#hours#') !== false) {
        if ($hours <> 0) {
          $format = preg_replace('/\{?([^{}]*)#hours#([^{}]*)\}?/s', '${1}'.$negative.$hours.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#hours#([^{}]*)\}/s', '/#hours#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#hour#([^{}]*)\}/s', '', $format);
      }
      // Because hours are affected by DST, we need to add to the timestamp, and not the date object.
      $startTimestamp += $hours * 3600;
      // Create a date object from the timestamp.
      $start_date = new DateTime(gmdate('c', (int) $startTimestamp));
      if (isset($timezone)) {
        if (!is_object($timezone)) {
          $timezone = new DateTimeZone($timezone);
        }
        $start_date->setTimezone($timezone);
      } else {
        $start_date->setTimezone(new DateTimeZone(date_default_timezone_get()));
      }
    }

    if (strpos($format, '#minute#') !== false || strpos($format, '#minutes#') !== false) {
      // Calculate number of minutes.
      $minutes = floor(($endTimestamp - $startTimestamp) / 60);
      // Minutes are constant, so we didn't go too far.
      if (strpos($format, '#minute#') !== false && $minutes == 1) {
        $format = preg_replace('/\{?([^{}]*)#minute#([^{}]*)\}?/s', '${1}'.$negative.$minutes.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#minutes#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#minutes#') !== false) {
        if ($minutes <> 0) {
          $format = preg_replace('/\{?([^{}]*)#minutes#([^{}]*)\}?/s', '${1}'.$negative.$minutes.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#minutes#([^{}]*)\}/s', '/#minutes#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#minute#([^{}]*)\}/s', '', $format);
      }
      // Because minutes are affected by DST, we need to add to the timestamp, and not the date object.
      $startTimestamp += $minutes * 60;
      // Create a date object from the timestamp.
      $start_date = new DateTime(gmdate('c', (int) $startTimestamp));
      if (isset($timezone)) {
        if (!is_object($timezone)) {
          $timezone = new DateTimeZone($timezone);
        }
        $start_date->setTimezone($timezone);
      } else {
        $start_date->setTimezone(new DateTimeZone(date_default_timezone_get()));
      }
    }

    if (strpos($format, '#second#') !== false || strpos($format, '#seconds#') !== false) {
      // Calculate number of seconds.
      $seconds = (int) $endTimestamp - (int) $startTimestamp;
      if (strpos($format, '#second#') !== false && $seconds == 1) {
        $format = preg_replace('/\{?([^{}]*)#second#([^{}]*)\}?/s', '${1}'.$negative.$seconds.'${2}', $format);
        $format = preg_replace('/\{([^{}]*)#seconds#([^{}]*)\}/s', '', $format);
      } elseif (strpos($format, '#seconds#') !== false) {
        if ($seconds <> 0) {
          $format = preg_replace('/\{?([^{}]*)#seconds#([^{}]*)\}?/s', '${1}'.$negative.$seconds.'${2}', $format);
        } else {
          $format = preg_replace(array('/\{([^{}]*)#seconds#([^{}]*)\}/s', '/#seconds#/'), array('', '0'), $format);
        }
        $format = preg_replace('/\{([^{}]*)#second#([^{}]*)\}/s', '', $format);
      }
    }

    return trim($format);
  }

  /**
   * Get a fuzzy time string.
   *
   * Converts a timestamp from the past into a human readable estimation of
   * the time that has passed.
   *
   * Ex: a few minutes ago
   *
   * Credit: http://www.byteinn.com/res/426/Fuzzy_Time_function/
   *
   * @param int $timestamp The timestamp to format.
   * @return string Fuzzy time string.
   */
  public static function formatFuzzyTime($timestamp) {
    $now = time();
    $one_minute = 60;
    $one_hour = 3600;
    $one_day = 86400;
    $one_week = $one_day * 7;
    $one_month = $one_day * 30.42;
    $one_year = $one_day * 365;

    // sod = start of day :)
    $sod = mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
    $sod_now = mktime(0, 0, 0, date('m', $now), date('d', $now), date('Y', $now));

    // used to convert numbers to strings
    $convert = array(
      1 => 'one',
      2 => 'two',
      3 => 'three',
      4 => 'four',
      5 => 'five',
      6 => 'six',
      7 => 'seven',
      8 => 'eight',
      9 => 'nine',
      10 => 'ten',
      11 => 'eleven',
      12 => 'twelve',
      13 => 'thirteen',
      14 => 'fourteen',
      15 => 'fifteen',
      16 => 'sixteen',
      17 => 'seventeen',
      18 => 'eighteen',
      19 => 'nineteen',
      20 => 'twenty',
    );

    // today (or yesterday, but less than 1 hour ago)
    if ($sod_now == $sod || $timestamp > $now - $one_hour) {
      if ($timestamp > $now - $one_minute) {
        return 'just now';
      } elseif ($timestamp > $now - ($one_minute * 3)) {
        return 'just a moment ago';
      } elseif ($timestamp > $now - ($one_minute * 7)) {
        return 'a few minutes ago';
      } elseif ($timestamp > $now - $one_hour) {
        return 'less than an hour ago';
      }
      return 'today at ' . date('g:ia', $timestamp);
    }

    // yesterday
    if (($sod_now - $sod) <= $one_day) {
      if (date('i', $timestamp) > ($one_minute + 30)) {
        $timestamp += $one_hour / 2;
      }
      return 'yesterday around ' . date('ga', $timestamp);
    }

    // within the last 5 days
    if (($sod_now - $sod) <= ($one_day * 5)) {
      $str = date('l', $timestamp);
      $hour = date('G', $timestamp);
      if ($hour < 12) {
        $str .= ' morning';
      } elseif ($hour < 17) {
        $str .= ' afternoon';
      } elseif ($hour < 20) {
        $str .= ' evening';
      } else {
        $str .= ' night';
      }
      return $str;
    }

    // number of weeks (between 1 and 3)...
    if (($sod_now - $sod) < ($one_week * 3.5)) {
      if (($sod_now - $sod) < ($one_week * 1.5)) {
        return 'about a week ago';
      } elseif (($sod_now - $sod) < ($one_day * 2.5)) {
        return 'about two weeks ago';
      } else {
        return 'about three weeks ago';
      }
    }

    // number of months (between 1 and 11)...
    if (($sod_now - $sod) < ($one_month * 11.5)) {
      for ($i = ($one_week * 3.5), $m = 0; $i < $one_year; $i += $one_month, $m++) {
        if (($sod_now - $sod) <= $i) {
          return 'about ' . $convert[$m] . ' month' . (($m > 1) ? 's' : '') . ' ago';
        }
      }
    }

    // number of years...
    for ($i = ($one_month * 11.5), $y = 0; $i < ($one_year * 21); $i += $one_year, $y++) {
      if (($sod_now - $sod) <= $i) {
        return 'about ' . $convert[$y] . ' year' . (($y > 1) ? 's' : '') . ' ago';
      }
    }

    // more than twenty years...
    return 'more than twenty years ago';
  }

  /**
   * Format a phone number.
   *
   * Uses US phone number format. E.g. "(800) 555-1234 x56".
   *
   * @param string $number The phone number to format.
   * @return string The formatted phone number.
   */
  public static function formatPhone($number) {
    if (!isset($number)) {
      return '';
    }
    $return = preg_replace('/\D*0?1?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d)?\D*(\d*)\D*/', '($1$2$3) $4$5$6-$7$8$9$10 x$11', (string) $number);
    return preg_replace('/\D*$/', '', $return);
  }
}