<?php

namespace Stampie;

use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\MultipartStream\MultipartStreamBuilder;
use Psr\Http\Message\ResponseInterface;
use Stampie\Util\IdentityUtils;

/**
 * Minimal implementation of a MailerInterface.
 *
 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
 */
abstract class Mailer implements MailerInterface
{
    /**
     * @var HttpClient
     */
    protected $httpClient;

    /**
     * @var string
     */
    protected $serverToken;

    /**
     * @var MessageFactory
     */
    private $messageFactory;

    /**
     * @param HttpClient $httpClient
     * @param string     $serverToken
     */
    public function __construct(HttpClient $httpClient, $serverToken)
    {
        $this->setHttpClient($httpClient);
        $this->setServerToken($serverToken);
    }

    /**
     * @param HttpClient $httpClient
     */
    public function setHttpClient(HttpClient $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    /**
     * @return HttpClient
     */
    private function getHttpClient()
    {
        return $this->httpClient;
    }

    /**
     * @return MessageFactory
     */
    private function getMessageFactory()
    {
        return $this->messageFactory ?: MessageFactoryDiscovery::find();
    }

    /**
     * @param MessageFactory $messageFactory
     *
     * @return Mailer
     */
    public function setMessageFactory(MessageFactory $messageFactory)
    {
        $this->messageFactory = $messageFactory;

        return $this;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     */
    public function setServerToken($serverToken)
    {
        if (empty($serverToken)) {
            throw new \InvalidArgumentException('ServerToken cannot be empty');
        }

        $this->serverToken = $serverToken;
    }

    /**
     * {@inheritdoc}
     */
    public function getServerToken()
    {
        return $this->serverToken;
    }

    /**
     * {@inheritdoc}
     */
    public function send(MessageInterface $message)
    {
        $response = $this->doSend($message);

        // We are all clear if status is HTTP 2xx OK
        if (in_array($response->getStatusCode(), range(200, 206), true)) {
            return;
        }

        $this->handle($response);
    }

    /**
     * Return a key -> value array of headers.
     *
     * example:
     *     array('X-Header-Name' => 'value')
     *
     * @return array
     */
    protected function getHeaders()
    {
        return [
            'Content-Type' => 'application/x-www-form-urlencoded',
        ];
    }

    /**
     * Return an key -> value array of files.
     *
     * example:
     *     ['foo_files' => array('attachmentname.jpg' => '/path/to/file.jpg')]
     *
     * @param MessageInterface $message
     *
     * @return string[]
     */
    protected function getFiles(MessageInterface $message)
    {
        return [];
    }

    /**
     * @return string
     */
    abstract protected function getEndpoint();

    /**
     * Return a a string formatted for the correct Mailer endpoint.
     * Postmark this is Json, SendGrid it is a urlencoded parameter list.
     *
     * @param MessageInterface $message
     *
     * @return string
     */
    abstract protected function format(MessageInterface $message);

    /**
     * If a Response is not successful it will be passed to this method
     * each Mailer should then throw an HttpException with an optional
     * ApiException to help identify the problem.
     *
     * @param ResponseInterface $response
     *
     * @throws \Stampie\Exception\ApiException
     * @throws \Stampie\Exception\HttpException
     */
    abstract protected function handle(ResponseInterface $response);

    /**
     * @param IdentityInterface|string $identity
     *
     * @return IdentityInterface
     */
    protected function normalizeIdentity($identity)
    {
        return IdentityUtils::normalizeIdentity($identity);
    }

    /**
     * @param IdentityInterface[]|string $identities
     *
     * @return IdentityInterface[]
     */
    protected function normalizeIdentities($identities)
    {
        return IdentityUtils::normalizeIdentities($identities);
    }

    /**
     * @param IdentityInterface[]|IdentityInterface|string $identities
     *
     * @return string
     */
    protected function buildIdentityString($identities)
    {
        return IdentityUtils::buildIdentityString($identities);
    }

    /**
     * Take a Message and return a PSR ResponseInterface.
     *
     * @param MessageInterface $message
     *
     * @return ResponseInterface
     */
    private function doSend(MessageInterface $message)
    {
        $content = $this->format($message);
        $headers = $this->getHeaders();
        $files = $this->getFiles($message);

        if (!empty($files)) {
            // HTTP query content
            parse_str($content, $fields);
            $builder = new MultipartStreamBuilder();

            foreach ($fields as $name => $value) {
                if (is_array($value)) {
                    foreach ($value as $c) {
                        $builder->addResource($name.'[]', $c);
                    }
                    continue;
                }

                $builder->addResource($name, $value);
            }

            // Add files to request
            foreach ($files as $key => $items) {
                foreach ($items as $name => $path) {
                    $options = [];
                    if (!is_numeric($name)) {
                        $options['filename'] = $name;
                    }
                    $value = fopen($path, 'r');
                    $builder->addResource($key, $value, $options);
                }
            }

            $content = $builder->build();
            $headers['Content-Type'] = 'multipart/form-data; boundary="'.$builder->getBoundary().'"';
        }

        $request = $this->getMessageFactory()->createRequest('POST', $this->getEndpoint(), $headers, $content);

        return $this->getHttpClient()->sendRequest($request);
    }
}


<?php

namespace Stampie;

/**
 * An Attachment is a container for a file that will be included with a Message.
 *
 * @author Adam Averay <adam@averay.com>
 */
class Attachment
{
    /**
     * @var string
     */
    protected $path;

    /**
     * @var string
     */
    protected $name;

    /**
     * @var string
     */
    protected $type;

    /**
     * @var string|null
     */
    protected $id;

    /**
     * @param string      $path
     * @param string|null $name
     * @param string|null $type
     * @param string|null $id
     *
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
    public function __construct($path, $name = null, $type = null, $id = null)
    {
        if (!$this->isValidFile($path)) {
            throw new \InvalidArgumentException('Cannot read file');
        }

        $this->path = $path;
        $this->name = (isset($name) ? $name : basename($path));
        $this->id = $id;

        if ($type === null) {
            $type = $this->determineFileType($path);
            if (!isset($type)) {
                throw new \RuntimeException('Cannot determine file type');
            }
        }

        $this->type = $type;
    }

    /**
     * @param string $path
     *
     * @return bool
     */
    protected function isValidFile($path)
    {
        return file_exists($path) && is_readable($path);
    }

    /**
     * @param string $path
     *
     * @throws \RuntimeException
     *
     * @return string
     */
    protected function determineFileType($path)
    {
        if (!function_exists('finfo_open')) {
            // File info functions not available
            return;
        }

        // Determine file type
        $finfo = finfo_open(\FILEINFO_MIME_TYPE);
        $type = finfo_file($finfo, $path);

        finfo_close($finfo);

        if ($type === false) {
            // Could not determine file type
            return;
        }

        return $type;
    }

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @return string|null
     */
    public function getId()
    {
        return $this->id;
    }
}


<?php

namespace Stampie\Mailer;

use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\ApiException;
use Stampie\Exception\HttpException;
use Stampie\IdentityInterface;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\MetadataAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;
use Stampie\Util\AttachmentUtils;

/**
 * @author Jérôme Parmentier <jerome@prmntr.me>
 */
class Mailjet extends Mailer
{
    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        return 'https://api.mailjet.com/v3.1/send';
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     */
    public function setServerToken($serverToken)
    {
        if (false === strpos($serverToken, ':')) {
            throw new \InvalidArgumentException('Mailjet uses a "publicApiKey:privateApiKey" based ServerToken');
        }

        parent::setServerToken($serverToken);
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        return [
            'Accept'        => 'application/json',
            'Authorization' => sprintf('Basic %s', base64_encode($this->getServerToken())),
            'Content-Type'  => 'application/json',
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        $parameters = [
            'From'     => $this->buildSenderField($message->getFrom()),
            'To'       => $this->buildRecipientsField($message->getTo()),
            'Cc'       => $this->buildRecipientsField($message->getCc()),
            'Bcc'      => $this->buildRecipientsField($message->getBcc()),
            'Subject'  => $message->getSubject(),
            'Headers'  => $message->getHeaders(),
            'HTMLPart' => $message->getHtml(),
            'TextPart' => $message->getText(),
            'ReplyTo'  => $this->buildSenderField($message->getReplyTo()),
        ];

        $attachments = $this->processAttachments($message);
        if ($attachments) {
            if ($attachments['attached']) {
                $parameters['Attachments'] = $attachments['attached'];
            }

            if ($attachments['inlined']) {
                $parameters['InlinedAttachments'] = $attachments['inlined'];
            }
        }

        if ($message instanceof MetadataAwareInterface) {
            $parameters['EventPayload'] = $message->getMetadata();
        }

        if ($message instanceof TaggableInterface) {
            $parameters['MonitoringCategory'] = $message->getTag();
        }

        return json_encode(['Messages' => [array_filter($parameters)]]);
    }

    /**
     * {@inheritdoc}
     */
    protected function handle(ResponseInterface $response)
    {
        $statusCode = $response->getStatusCode();
        $httpException = new HttpException($statusCode, $response->getReasonPhrase());

        if (!in_array($statusCode, [400, 401, 403], true)) {
            throw $httpException;
        }

        $error = json_decode((string) $response->getBody(), true);

        if (isset($error['ErrorMessage'])) {
            throw new ApiException($error['ErrorMessage'], $httpException);
        }

        $errorMessages = [];
        foreach ($error['Messages'] as $message) {
            if ('error' !== $message['Status']) {
                continue;
            }

            foreach ($message['Errors'] as $mailError) {
                $errorMessages[] = $mailError['ErrorMessage'];
            }
        }

        throw new ApiException(implode(', ', $errorMessages), $httpException);
    }

    /**
     * @param IdentityInterface|string $identity
     *
     * @return array
     */
    protected function buildSenderField($identity)
    {
        if (null === $identity) {
            return [];
        }

        if (is_string($identity)) {
            return [
                'Email' => $identity,
            ];
        }

        $sender = [
            'Email' => $identity->getEmail(),
        ];

        if (null !== $name = $identity->getName()) {
            $sender['Name'] = $name;
        }

        return $sender;
    }

    /**
     * @param IdentityInterface[]|string $identities
     *
     * @return array
     */
    protected function buildRecipientsField($identities)
    {
        if (null === $identities) {
            return [];
        }

        if (is_string($identities)) {
            return [
                [
                    'Email' => $identities,
                ],
            ];
        }

        $identities = (array) $identities;

        $recipients = [];
        foreach ($identities as $identity) {
            if (is_string($identity)) {
                $recipients[] = [
                    'Email' => $identity,
                ];

                continue;
            }

            $recipient = [
                'Email' => $identity->getEmail(),
            ];

            if (null !== $name = $identity->getName()) {
                $recipient['Name'] = $name;
            }

            $recipients[] = $recipient;
        }

        return $recipients;
    }

    protected function processAttachments(MessageInterface $message)
    {
        if (!$message instanceof AttachmentsAwareInterface) {
            return [];
        }

        $processedAttachments = [
            'attached' => [],
            'inlined'  => [],
        ];

        $attachments = AttachmentUtils::processAttachments($message->getAttachments());
        foreach ($attachments as $name => $attachment) {
            $item = [
                'Filename'      => $name,
                'Base64Content' => base64_encode($this->getAttachmentContent($attachment)),
                'ContentType'   => $attachment->getType(),
            ];

            if (null !== $id = $attachment->getId()) {
                $item['ContentID'] = $id;
                $processedAttachments['inlined'][] = $item;

                continue;
            }

            $processedAttachments['attached'][] = $item;
        }

        return $processedAttachments;
    }

    /**
     * @param Attachment $attachment
     *
     * @return string
     */
    protected function getAttachmentContent(Attachment $attachment)
    {
        return file_get_contents($attachment->getPath());
    }
}


<?php

namespace Stampie\Mailer;

use Http\Client\HttpClient;
use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\ApiException;
use Stampie\Exception\HttpException;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\MetadataAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;
use Stampie\Util\IdentityUtils;

/**
 * Mailer to be used with SparkPost's API.
 *
 * @author Shaun Simmons <shaun@envysphere.com>
 */
class SparkPost extends Mailer
{
    private $transactional;

    /**
     * @param HttpClient $httpClient
     * @param string     $serverToken
     * @param bool       $transactional Whether messages are transactional for unsubscribe and suppression purposes
     *
     * @see https://en.wikipedia.org/wiki/Email_marketing#Transactional_emails
     */
    public function __construct(HttpClient $httpClient, $serverToken, $transactional = true)
    {
        parent::__construct($httpClient, $serverToken);
        $this->transactional = (bool) $transactional;
    }

    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        return 'https://api.sparkpost.com/api/v1/transmissions';
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        return [
            'Content-Type'  => 'application/json',
            'Authorization' => $this->getServerToken(),
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        $from = $this->normalizeIdentity($message->getFrom());

        $parameters = [
            'options' => [
                'transactional' => $this->transactional,
            ],
            'content' => [
                'from' => [
                    'name'  => $from->getName(),
                    'email' => $from->getEmail(),
                ],
                'subject' => $message->getSubject(),
                'text'    => $message->getText(),
                'html'    => $message->getHtml(),
            ],
        ];

        if ($message->getReplyTo()) {
            $parameters['content']['reply_to'] = $message->getReplyTo();
        }

        if (count($message->getHeaders())) {
            $parameters['content']['headers'] = $message->getHeaders();
        }

        if ($message instanceof AttachmentsAwareInterface) {
            foreach ($message->getAttachments() as $attachment) {
                $inline = $attachment->getId() !== null;
                $parameters['content'][$inline ? 'inline_images' : 'attachments'][] = [
                    'type' => $attachment->getType(),
                    'name' => $inline ? $attachment->getId() : $attachment->getName(),
                    'data' => base64_encode($this->getAttachmentContent($attachment)),
                ];
            }
        }

        if ($message instanceof MetadataAwareInterface) {
            $metadata = array_filter($message->getMetadata());
            if (count($metadata)) {
                $parameters['metadata'] = $metadata;
            }
        }

        $tags = [];
        if ($message instanceof TaggableInterface) {
            $tags = (array) $message->getTag();
        }

        $normalizedTo = $this->normalizeIdentities($message->getTo());
        $normalizedCc = $this->normalizeIdentities($message->getCc());
        $normalizedBcc = $this->normalizeIdentities($message->getBcc());
        $toIdentityString = IdentityUtils::buildIdentityString($normalizedTo);

        /** @var \Stampie\Identity[] $merged */
        $merged = array_merge($normalizedTo, $normalizedCc, $normalizedBcc);
        foreach ($merged as $recipient) {
            /*
             * The reason we are specifying header_to is so that each recipient can see *everybody* who received
             * the email. Without it, each recipient will only see their email in the To field.
             *
             * IMPORTANT: Do not specify address.name!
             * If given, SparkPost will try to wrap the entire header_to value, mangling it in the process.
             */

            $parameters['recipients'][] = [
                'address' => [
                    'email'     => $recipient->getEmail(),
                    'header_to' => $toIdentityString,
                ],
                'tags' => $tags,
            ];
        }

        if (count($normalizedCc)) {
            $parameters['content']['headers']['CC'] = IdentityUtils::buildIdentityString($normalizedCc);
        }

        return json_encode($parameters);
    }

    /**
     * {@inheritdoc}
     *
     * HTTP codes 400-429 will throw an ApiException, otherwise an HttpException is thrown.
     */
    protected function handle(ResponseInterface $response)
    {
        $httpException = new HttpException($response->getStatusCode(), $response->getReasonPhrase());

        // 400-429 will contain error information in the body encoded as JSON
        if ($response->getStatusCode() >= 400 && $response->getStatusCode() <= 429) {
            throw new ApiException((string) $response->getBody(), $httpException);
        }

        throw $httpException;
    }

    private function getAttachmentContent(Attachment $attachment)
    {
        return file_get_contents($attachment->getPath());
    }
}


<?php

namespace Stampie\Mailer;

use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\ApiException;
use Stampie\Exception\HttpException;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\MetadataAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;
use Stampie\Util\AttachmentUtils;

/**
 * Sends emails to Mandrill server.
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class Mandrill extends Mailer
{
    /**
     * @var string
     */
    private $subaccount;

    /**
     * @param string $subaccount
     */
    public function setSubaccount($subaccount)
    {
        $this->subaccount = $subaccount;
    }

    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        return 'https://mandrillapp.com/api/1.0/messages/send.json';
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        return [
            'Content-Type' => 'application/json',
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        $headers = array_filter(array_merge(
            $message->getHeaders(),
            ['Reply-To' => $message->getReplyTo()]
        ));

        $from = $this->normalizeIdentity($message->getFrom());

        $to = [];
        foreach ($this->normalizeIdentities($message->getTo()) as $recipient) {
            $to[] = ['email' => $recipient->getEmail(), 'name' => $recipient->getName(), 'type' => 'to'];
        }

        foreach ($this->normalizeIdentities($message->getCc()) as $recipient) {
            $to[] = ['email' => $recipient->getEmail(), 'name' => $recipient->getName(), 'type' => 'cc'];
        }

        foreach ($this->normalizeIdentities($message->getBcc()) as $recipient) {
            $to[] = ['email' => $recipient->getEmail(), 'name' => $recipient->getName(), 'type' => 'bcc'];
        }

        $tags = [];
        if ($message instanceof TaggableInterface) {
            $tags = (array) $message->getTag();
        }

        $metadata = [];
        if ($message instanceof MetadataAwareInterface) {
            $metadata = array_filter($message->getMetadata());
        }

        $images = [];
        $attachments = [];
        if ($message instanceof AttachmentsAwareInterface) {
            list($attachments, $images) = $this->processAttachments($message->getAttachments());
        }

        $parameters = [
            'key'     => $this->getServerToken(),
            'message' => array_filter([
                'from_email'  => $from->getEmail(),
                'from_name'   => $from->getName(),
                'to'          => $to,
                'subject'     => $message->getSubject(),
                'headers'     => $headers,
                'text'        => $message->getText(),
                'html'        => $message->getHtml(),
                'tags'        => $tags,
                'metadata'    => $metadata,
                'attachments' => $attachments,
                'images'      => $images,
            ]),
        ];

        if ($this->subaccount) {
            $parameters['message']['subaccount'] = $this->subaccount;
        }

        return json_encode($parameters);
    }

    /**
     * {@inheritdoc}
     *
     * "You can consider any non-200 HTTP response code an error - the returned data will contain more detailed information"
     */
    protected function handle(ResponseInterface $response)
    {
        $httpException = new HttpException($response->getStatusCode(), $response->getReasonPhrase());
        $error = json_decode((string) $response->getBody());

        throw new ApiException($error->message, $httpException, $error->code);
    }

    /**
     * @param Attachment[] $attachments
     *
     * @return array
     *               First element: Attachments – an array containing arrays of the following format
     *               array(
     *               'type'    => type,
     *               'name'    => name,
     *               'content' => base64-encoded content,
     *               )
     *
     *     Second element: Inline images – an array containing arrays of the following format
     *         array(
     *             'type'    => type,
     *             'name'    => id,
     *             'content' => base64-encoded content,
     *         )
     */
    protected function processAttachments(array $attachments)
    {
        $attachments = AttachmentUtils::processAttachments($attachments);

        $processedAttachments = [];
        $images = [];
        foreach ($attachments as $name => $attachment) {
            $type = $attachment->getType();
            $item = [
                'type'    => $type,
                'name'    => $name,
                'content' => base64_encode($this->getAttachmentContent($attachment)),
            ];

            $id = $attachment->getId();
            if (strpos($type, 'image/') === 0 && isset($id)) {
                // Inline image
                $item['name'] = $id;
                $images[] = $item;
            } else {
                // Attached
                $processedAttachments[] = $item;
            }
        }

        return [$processedAttachments, $images];
    }

    /**
     * @param Attachment $attachment
     *
     * @return string
     */
    protected function getAttachmentContent(Attachment $attachment)
    {
        return file_get_contents($attachment->getPath());
    }
}


<?php

namespace Stampie\Mailer;

use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\ApiException;
use Stampie\Exception\HttpException;
use Stampie\IdentityInterface;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\MetadataAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;
use Stampie\Util\AttachmentUtils;

/**
 * Mailer to be used with SendGrid Web API.
 *
 * @author Henrik Bjrnskov <henrik@bjrnskov.dk>
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class SendGrid extends Mailer
{
    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        return 'https://api.sendgrid.com/v3/mail/send';
    }

    /**
     * {@inheritdoc}
     */
    protected function handle(ResponseInterface $response)
    {
        $httpException = new HttpException($response->getStatusCode(), $response->getReasonPhrase());

        // 4xx will content error information in the body encoded as JSON
        if (!in_array($response->getStatusCode(), range(400, 417))) {
            throw $httpException;
        }

        $error = json_decode((string) $response->getBody());
        $message = '';
        foreach ($error->errors as $i => $e) {
            $message .= sprintf(
                "ERROR #%d: \nField: %s \nMessage: %s \nHelp: %s\n\n\n",
                $i,
                isset($e->field) ? $e->field : '-',
                isset($e->message) ? $e->message : '-',
                isset($e->help) ? $e->help : '-'
            );
        }

        throw new ApiException($message, $httpException);
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        return [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer '.$this->getServerToken(),
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        $personalization = [
            'to'      => $this->formatRecipients($message->getTo()),
            'cc'      => $this->formatRecipients($message->getCc()),
            'bcc'     => $this->formatRecipients($message->getBcc()),
            'subject' => $message->getSubject(),
        ];

        if ($headers = $message->getHeaders()) {
            $personalization['headers'] = $headers;
        }

        if (empty($personalization['cc'])) {
            unset($personalization['cc']);
        }

        if (empty($personalization['bcc'])) {
            unset($personalization['bcc']);
        }

        $content = [
            ['type' => 'text/plain', 'value' => $message->getText()],
            ['type' => 'text/html', 'value' => $message->getHtml()],
        ];

        foreach ($content as $i => $c) {
            if (empty($c['value'])) {
                unset($content[$i]);
            }
        }

        $parameters = [
            'personalizations' => [$personalization],
            'from'             => $this->formatRecipients($message->getFrom())[0],
            'reply_to'         => $this->formatRecipients($message->getReplyTo()),
            'content'          => array_values($content),
        ];

        if (empty($parameters['reply_to'])) {
            unset($parameters['reply_to']);
        } else {
            $parameters['reply_to'] = $parameters['reply_to'][0];
        }

        if ($message instanceof AttachmentsAwareInterface) {
            $attachments = $message->getAttachments();
            if (!empty($attachments)) {
                $parameters['attachments'] = $this->processAttachments($attachments);
            }
        }

        if ($message instanceof TaggableInterface) {
            $parameters['categories'] = (array) $message->getTag();
        }

        if ($message instanceof MetadataAwareInterface) {
            $metadata = array_filter($message->getMetadata());
            if (!empty($metadata)) {
                if (isset($parameters['custom_args'])) {
                    $parameters['custom_args'] = array_merge($parameters['custom_args'], $metadata);
                } else {
                    $parameters['custom_args'] = $metadata;
                }
            }
        }

        return json_encode($parameters);
    }

    /**
     * @param Attachment[] $attachments
     *
     * @return array First element: All attachments – array(name => path). Second element: Inline attachments – array(id => name)
     */
    protected function processAttachments(array $attachments)
    {
        $attachments = AttachmentUtils::processAttachments($attachments);
        $processedAttachments = [];

        foreach ($attachments as $name => $attachment) {
            $item = [
                'content'  => base64_encode(file_get_contents($attachment->getPath())),
                'type'     => $attachment->getType(),
                'filename' => $attachment->getName(),
            ];

            $id = $attachment->getId();
            if (!empty($id)) {
                $item['disposition'] = 'inline';
                $item['content_id'] = $id;
            }

            $processedAttachments[] = $item;
        }

        return $processedAttachments;
    }

    /**
     * @param IdentityInterface[]|string $recipients
     *
     * @return array
     */
    private function formatRecipients($recipients)
    {
        $data = [];
        foreach ($this->normalizeIdentities($recipients) as $recipient) {
            $item = [
                'email' => $recipient->getEmail(),
                'name'  => $recipient->getName(),
            ];

            if (empty($item['name'])) {
                unset($item['name']);
            }

            $data[] = $item;
        }

        return $data;
    }
}


<?php

namespace Stampie\Util;

use Stampie\Identity;
use Stampie\IdentityInterface;

/**
 * Stampie Identity utility functions.
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class IdentityUtils
{
    /**
     * This class should not be instantiated.
     */
    private function __construct()
    {
    }

    /**
     * @param IdentityInterface|string $identity
     *
     * @return IdentityInterface
     */
    public static function normalizeIdentity($identity)
    {
        if (!$identity instanceof IdentityInterface) {
            $identity = new Identity($identity);
        }

        return $identity;
    }

    /**
     * @param IdentityInterface[]|string $identities
     *
     * @return IdentityInterface[]
     */
    public static function normalizeIdentities($identities)
    {
        if (is_array($identities)) {
            $identities = array_map([__CLASS__, 'normalizeIdentity'], $identities);
        }

        if (null === $identities) {
            return [];
        }

        if (is_string($identities)) {
            $identities = [self::normalizeIdentity($identities)];
        }

        if (!is_array($identities) && $identities instanceof IdentityInterface) {
            $identities = [$identities];
        }

        return $identities;
    }

    /**
     * @param IdentityInterface[]|IdentityInterface|string $identities
     *
     * @return string
     */
    public static function buildIdentityString($identities)
    {
        if (null === $identities) {
            return '';
        }

        if (is_string($identities)) {
            return $identities;
        }

        if ($identities instanceof IdentityInterface) {
            $identities = [$identities];
        }

        $stringIdentities = [];

        foreach ($identities as $identity) {
            if (null === $identity->getName()) {
                $stringIdentities[] = $identity->getEmail();
                continue;
            }

            $stringIdentities[] = sprintf('%s <%s>', $identity->getName(), $identity->getEmail());
        }

        return implode(',', $stringIdentities);
    }
}


<?php

namespace Stampie\Mailer;

use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\HttpException;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\MetadataAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;

/**
 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
 */
class MailGun extends Mailer
{
    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        list($domain) = explode(':', $this->getServerToken());

        return 'https://api.mailgun.net/v2/'.$domain.'/messages';
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     */
    public function setServerToken($serverToken)
    {
        if (false === strpos($serverToken, ':')) {
            throw new \InvalidArgumentException('MailGun uses a "custom.domain.tld:key-hash" based ServerToken');
        }

        parent::setServerToken($serverToken);
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        list(, $serverToken) = explode(':', $this->getServerToken());

        return [
            'Authorization' => 'Basic '.base64_encode('api:'.$serverToken),
            'Content-Type'  => 'application/x-www-form-urlencoded',
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function getFiles(MessageInterface $message)
    {
        if (!($message instanceof AttachmentsAwareInterface)) {
            return [];
        }

        // Process files
        list($attachments, $inline) = $this->processAttachments($message->getAttachments());

        // Format params
        $files = [];
        if ($attachments) {
            $files['attachment'] = $attachments;
        }
        if ($inline) {
            $files['inline'] = $inline;
        }

        return $files;
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        // Custom headers should be prefixed with h:X-My-Header
        $headers = array_merge(
            $message->getHeaders(),
            ['Reply-To' => $message->getReplyTo()]
        );
        array_walk($headers, function (&$value, &$key) {
            $key = 'h:'.$key;
        });

        $parameters = [
            'from'    => $this->buildIdentityString($message->getFrom()),
            'to'      => $this->buildIdentityString($message->getTo()),
            'subject' => $message->getSubject(),
            'text'    => $message->getText(),
            'html'    => $message->getHtml(),
            'cc'      => $this->buildIdentityString($message->getCc()),
            'bcc'     => $this->buildIdentityString($message->getBcc()),
        ];

        if ($message instanceof TaggableInterface) {
            $parameters['o:tag'] = (array) $message->getTag();
        }

        $metadata = [];
        if ($message instanceof MetadataAwareInterface) {
            $metadata = array_filter($message->getMetadata());
            // Custom variables should be prefixed with v:my_var
            array_walk($metadata, function (&$value, &$key) {
                $key = 'v:'.$key;
            });
        }

        return http_build_query(array_filter(array_merge($headers, $parameters, $metadata)));
    }

    /**
     * {@inheritdoc}
     */
    protected function handle(ResponseInterface $response)
    {
        throw new HttpException($response->getStatusCode(), $response->getReasonPhrase());
    }

    /**
     * @param Attachment[] $attachments
     *
     * @return array First element: An array of attachment paths. Second element: An array of inline paths
     */
    protected function processAttachments(array $attachments)
    {
        $processedAttachments = [];
        $inline = [];
        foreach ($attachments as $attachment) {
            $path = $attachment->getPath();
            $id = $attachment->getId();
            if (isset($id)) {
                // Inline
                $inline[] = $path;
            } else {
                // Attached
                $processedAttachments[$attachment->getName()] = $path;
            }
        }

        return [$processedAttachments, $inline];
    }
}


<?php

namespace Stampie\Util;

use Stampie\Attachment;

/**
 * Stampie Attachment utility functions.
 *
 * @author Adam Averay <adam@averay.com>
 */
final class AttachmentUtils
{
    /**
     * This class should not be instantiated.
     */
    private function __construct()
    {
    }

    /**
     * Applies a function to each attachment, and finds a unique name for any conflicting names.
     *
     * @param Attachment[] $attachments
     *
     * @throws \InvalidArgumentException
     *
     * @return Attachment[]
     */
    public static function processAttachments(array $attachments)
    {
        $processed = [];

        foreach ($attachments as $attachment) {
            if (!($attachment instanceof Attachment)) {
                throw new \InvalidArgumentException('Attachments must implement Stampie\\Attachment');
            }

            $name = $attachment->getName();

            if (isset($processed[$name])) {
                // Name conflict
                $name = static::findUniqueName($name, array_keys($processed));
            }

            $processed[$name] = $attachment;
        }

        return $processed;
    }

    /**
     * @param string $name    The name to make unique
     * @param array  $claimed Names already in use to avoid
     *
     * @return string A unique name
     */
    public static function findUniqueName($name, array $claimed)
    {
        $ext = pathinfo($name, \PATHINFO_EXTENSION);
        $basename = substr($name, 0, -strlen('.'.$ext));

        $i = 0;
        while (in_array($name, $claimed)) {
            $i++;
            $name = $basename.'-'.$i.'.'.$ext;
        }

        return $name;
    }
}


<?php

namespace Stampie\Mailer;

use Psr\Http\Message\ResponseInterface;
use Stampie\Attachment;
use Stampie\Exception\ApiException;
use Stampie\Exception\HttpException;
use Stampie\Mailer;
use Stampie\Message\AttachmentsAwareInterface;
use Stampie\Message\TaggableInterface;
use Stampie\MessageInterface;
use Stampie\Util\AttachmentUtils;

/**
 * Sends emails to Postmark server.
 *
 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
 */
class Postmark extends Mailer
{
    /**
     * {@inheritdoc}
     */
    protected function getEndpoint()
    {
        return 'http://api.postmarkapp.com/email';
    }

    /**
     * {@inheritdoc}
     */
    protected function handle(ResponseInterface $response)
    {
        $httpException = new HttpException($response->getStatusCode(), $response->getReasonPhrase());

        // Not 422 contains information about API Error
        if ($response->getStatusCode() == 422) {
            $error = json_decode((string) $response->getBody());

            throw new ApiException(isset($error->Message) ? $error->Message : 'Unprocessable Entity', $httpException);
        }

        throw $httpException;
    }

    /**
     * {@inheritdoc}
     */
    protected function getHeaders()
    {
        return [
            'Content-Type'            => 'application/json',
            'X-Postmark-Server-Token' => $this->getServerToken(),
            'Accept'                  => 'application/json',
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function format(MessageInterface $message)
    {
        $headers = [];
        foreach ($message->getHeaders() as $name => $value) {
            $headers[] = ['Name' => $name, 'Value' => $value];
        }

        $parameters = [
            'From'     => $this->buildIdentityString($message->getFrom()),
            'To'       => $this->buildIdentityString($message->getTo()),
            'Subject'  => $message->getSubject(),
            'Headers'  => $headers,
            'HtmlBody' => $message->getHtml(),
            'TextBody' => $message->getText(),
            'ReplyTo'  => $message->getReplyTo(),
        ];

        if ($message instanceof TaggableInterface) {
            $tag = $message->getTag();

            if (is_array($tag)) {
                $tag = reset($tag);
            }

            $parameters['Tag'] = $tag;
        }

        if ($message instanceof AttachmentsAwareInterface) {
            $attachments = $this->processAttachments($message->getAttachments());

            if ($attachments) {
                $parameters['Attachments'] = $attachments;
            }
        }

        return json_encode(array_filter($parameters));
    }

    /**
     * @param Attachment $attachment
     *
     * @return string
     */
    protected function getAttachmentContent(Attachment $attachment)
    {
        return file_get_contents($attachment->getPath());
    }

    /**
     * @param Attachment[] $attachments
     *
     * @return array An array containing arrays of the following format:
     *               array(
     *               'Name'                 => name,
     *               'Content'              => base64-encoded content,
     *               'ContentType'          => type,
     *               (optional) 'ContentID' => id,
     *               )
     */
    protected function processAttachments(array $attachments)
    {
        $attachments = AttachmentUtils::processAttachments($attachments);

        $processedAttachments = [];
        foreach ($attachments as $name => $attachment) {
            $item = [
                'Name'        => $name,
                'Content'     => base64_encode($this->getAttachmentContent($attachment)),
                'ContentType' => $attachment->getType(),
            ];

            $id = $attachment->getId();
            if (isset($id)) {
                $item['ContentID'] = $id;
            }

            $processedAttachments[] = $item;
        }

        return $processedAttachments;
    }
}


<?php namespace SuperClosure\Analyzer;

use SuperClosure\Analyzer\Visitor\ThisDetectorVisitor;
use SuperClosure\Exception\ClosureAnalysisException;
use SuperClosure\Analyzer\Visitor\ClosureLocatorVisitor;
use SuperClosure\Analyzer\Visitor\MagicConstantVisitor;
use PhpParser\NodeTraverser;
use PhpParser\PrettyPrinter\Standard as NodePrinter;
use PhpParser\Error as ParserError;
use PhpParser\Node\Expr\Variable as VariableNode;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser as CodeParser;
use PhpParser\ParserFactory;
use PhpParser\Lexer\Emulative as EmulativeLexer;

/**
 * This is the AST based analyzer.
 *
 * We're using reflection and AST-based code parser to analyze a closure and
 * determine its code and context using the nikic/php-parser library. The AST
 * based analyzer and has more capabilities than the token analyzer, but is,
 * unfortunately, about 25 times slower.
 */
class AstAnalyzer extends ClosureAnalyzer
{
    protected function determineCode(array &$data)
    {
        // Find the closure by traversing through a AST of the code.
        // Note: This also resolves class names to their FQCNs while traversing.
        $this->locateClosure($data);

        // Make a second pass through the AST, but only through the closure's
        // nodes, to resolve any magic constants to literal values.
        $traverser = new NodeTraverser;
        $traverser->addVisitor(new MagicConstantVisitor($data['location']));
        $traverser->addVisitor($thisDetector = new ThisDetectorVisitor);
        $data['ast'] = $traverser->traverse([$data['ast']])[0];
        $data['hasThis'] = $thisDetector->detected;

        // Bounce the updated AST down to a string representation of the code.
        $data['code'] = (new NodePrinter)->prettyPrint([$data['ast']]);
    }

    /**
     * Parses the closure's code and produces an abstract syntax tree (AST).
     *
     * @param array $data
     *
     * @throws ClosureAnalysisException if there is an issue finding the closure
     */
    private function locateClosure(array &$data)
    {
        try {
            $locator = new ClosureLocatorVisitor($data['reflection']);
            $fileAst = $this->getFileAst($data['reflection']);

            $fileTraverser = new NodeTraverser;
            $fileTraverser->addVisitor(new NameResolver);
            $fileTraverser->addVisitor($locator);
            $fileTraverser->traverse($fileAst);
        } catch (ParserError $e) {
            // @codeCoverageIgnoreStart
            throw new ClosureAnalysisException(
                'There was an error analyzing the closure code.', 0, $e
            );
            // @codeCoverageIgnoreEnd
        }

        $data['ast'] = $locator->closureNode;
        if (!$data['ast']) {
            // @codeCoverageIgnoreStart
            throw new ClosureAnalysisException(
                'The closure was not found within the abstract syntax tree.'
            );
            // @codeCoverageIgnoreEnd
        }

        $data['location'] = $locator->location;
    }

    /**
     * Returns the variables that in the "use" clause of the closure definition.
     * These are referred to as the "used variables", "static variables", or
     * "closed upon variables", "context" of the closure.
     *
     * @param array $data
     */
    protected function determineContext(array &$data)
    {
        // Get the variable names defined in the AST
        $refs = 0;
        $vars = array_map(function ($node) use (&$refs) {
            if ($node->byRef) {
                $refs++;
            }
            if ($node->var instanceof VariableNode) {
                // For PHP-Parser >=4.0
                return $node->var->name;
            } else {
                // For PHP-Parser <4.0
                return $node->var;
            }
        }, $data['ast']->uses);
        $data['hasRefs'] = ($refs > 0);

        // Get the variable names and values using reflection
        $values = $data['reflection']->getStaticVariables();

        // Combine the names and values to create the canonical context.
        foreach ($vars as $name) {
            if (isset($values[$name])) {
                $data['context'][$name] = $values[$name];
            }
        }
    }

    /**
     * @param \ReflectionFunction $reflection
     *
     * @throws ClosureAnalysisException
     *
     * @return \PhpParser\Node[]
     */
    private function getFileAst(\ReflectionFunction $reflection)
    {
        $fileName = $reflection->getFileName();
        if (!file_exists($fileName)) {
            throw new ClosureAnalysisException(
                "The file containing the closure, \"{$fileName}\" did not exist."
            );
        }

        return $this->getParser()->parse(file_get_contents($fileName));
    }

    /**
     * @return CodeParser
     */
    private function getParser()
    {
        if (class_exists('PhpParser\ParserFactory')) {
            return (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
        }

        return new CodeParser(new EmulativeLexer);
    }
}


<?php namespace SuperClosure;

use SuperClosure\Analyzer\AstAnalyzer as DefaultAnalyzer;
use SuperClosure\Analyzer\ClosureAnalyzer;
use SuperClosure\Exception\ClosureSerializationException;
use SuperClosure\Exception\ClosureUnserializationException;

/**
 * This is the serializer class used for serializing Closure objects.
 *
 * We're abstracting away all the details, impossibilities, and scary things
 * that happen within.
 */
class Serializer implements SerializerInterface
{
    /**
     * The special value marking a recursive reference to a closure.
     *
     * @var string
     */
    const RECURSION = "{{RECURSION}}";

    /**
     * The keys of closure data required for serialization.
     *
     * @var array
     */
    private static $dataToKeep = [
        'code'     => true,
        'context'  => true,
        'binding'  => true,
        'scope'    => true,
        'isStatic' => true,
    ];

    /**
     * The closure analyzer instance.
     *
     * @var ClosureAnalyzer
     */
    private $analyzer;

    /**
     * The HMAC key to sign serialized closures.
     *
     * @var string
     */
    private $signingKey;

    /**
     * Create a new serializer instance.
     *
     * @param ClosureAnalyzer|null $analyzer   Closure analyzer instance.
     * @param string|null          $signingKey HMAC key to sign closure data.
     */
    public function __construct(
        ClosureAnalyzer $analyzer = null,
        $signingKey = null
    ) {
        $this->analyzer = $analyzer ?: new DefaultAnalyzer;
        $this->signingKey = $signingKey;
    }

    /**
     * @inheritDoc
     */
    public function serialize(\Closure $closure)
    {
        $serialized = serialize(new SerializableClosure($closure, $this));

        if ($serialized === null) {
            throw new ClosureSerializationException(
                'The closure could not be serialized.'
            );
        }

        if ($this->signingKey) {
            $signature = $this->calculateSignature($serialized);
            $serialized = '%' . base64_encode($signature) . $serialized;
        }

        return $serialized;
    }

    /**
     * @inheritDoc
     */
    public function unserialize($serialized)
    {
        // Strip off the signature from the front of the string.
        $signature = null;
        if ($serialized[0] === '%') {
            $signature = base64_decode(substr($serialized, 1, 44));
            $serialized = substr($serialized, 45);
        }

        // If a key was provided, then verify the signature.
        if ($this->signingKey) {
            $this->verifySignature($signature, $serialized);
        }

        set_error_handler(function () {});
        $unserialized = unserialize($serialized);
        restore_error_handler();
        if ($unserialized === false) {
            throw new ClosureUnserializationException(
                'The closure could not be unserialized.'
            );
        } elseif (!$unserialized instanceof SerializableClosure) {
            throw new ClosureUnserializationException(
                'The closure did not unserialize to a SuperClosure.'
            );
        }

        return $unserialized->getClosure();
    }

    /**
     * @inheritDoc
     */
    public function getData(\Closure $closure, $forSerialization = false)
    {
        // Use the closure analyzer to get data about the closure.
        $data = $this->analyzer->analyze($closure);

        // If the closure data is getting retrieved solely for the purpose of
        // serializing the closure, then make some modifications to the data.
        if ($forSerialization) {
            // If there is no reference to the binding, don't serialize it.
            if (!$data['hasThis']) {
                $data['binding'] = null;
            }

            // Remove data about the closure that does not get serialized.
            $data = array_intersect_key($data, self::$dataToKeep);

            // Wrap any other closures within the context.
            foreach ($data['context'] as &$value) {
                if ($value instanceof \Closure) {
                    $value = ($value === $closure)
                        ? self::RECURSION
                        : new SerializableClosure($value, $this);
                }
            }
        }

        return $data;
    }

    /**
     * Recursively traverses and wraps all Closure objects within the value.
     *
     * NOTE: THIS MAY NOT WORK IN ALL USE CASES, SO USE AT YOUR OWN RISK.
     *
     * @param mixed $data Any variable that contains closures.
     * @param SerializerInterface $serializer The serializer to use.
     */
    public static function wrapClosures(&$data, SerializerInterface $serializer)
    {
        if ($data instanceof \Closure) {
            // Handle and wrap closure objects.
            $reflection = new \ReflectionFunction($data);
            if ($binding = $reflection->getClosureThis()) {
                self::wrapClosures($binding, $serializer);
                $scope = $reflection->getClosureScopeClass();
                $scope = $scope ? $scope->getName() : 'static';
                $data = $data->bindTo($binding, $scope);
            }
            $data = new SerializableClosure($data, $serializer);
        } elseif (is_array($data) || $data instanceof \stdClass || $data instanceof \Traversable) {
            // Handle members of traversable values.
            foreach ($data as &$value) {
                self::wrapClosures($value, $serializer);
            }
        } elseif (is_object($data) && !$data instanceof \Serializable) {
            // Handle objects that are not already explicitly serializable.
            $reflection = new \ReflectionObject($data);
            if (!$reflection->hasMethod('__sleep')) {
                foreach ($reflection->getProperties() as $property) {
                    if ($property->isPrivate() || $property->isProtected()) {
                        $property->setAccessible(true);
                    }
                    $value = $property->getValue($data);
                    self::wrapClosures($value, $serializer);
                    $property->setValue($data, $value);
                }
            }
        }
    }

    /**
     * Calculates a signature for a closure's serialized data.
     *
     * @param string $data Serialized closure data.
     *
     * @return string Signature of the closure's data.
     */
    private function calculateSignature($data)
    {
        return hash_hmac('sha256', $data, $this->signingKey, true);
    }

    /**
     * Verifies the signature for a closure's serialized data.
     *
     * @param string $signature The provided signature of the data.
     * @param string $data      The data for which to verify the signature.
     *
     * @throws ClosureUnserializationException if the signature is invalid.
     */
    private function verifySignature($signature, $data)
    {
        // Verify that the provided signature matches the calculated signature.
        if (!hash_equals($signature, $this->calculateSignature($data))) {
            throw new ClosureUnserializationException('The signature of the'
                . ' closure\'s data is invalid, which means the serialized '
                . 'closure has been modified and is unsafe to unserialize.'
            );
        }
    }
}


<?php namespace SuperClosure\Analyzer;

use SuperClosure\Exception\ClosureAnalysisException;

abstract class ClosureAnalyzer
{
    /**
     * Analyzer a given closure.
     *
     * @param \Closure $closure
     *
     * @throws ClosureAnalysisException
     *
     * @return array
     */
    public function analyze(\Closure $closure)
    {
        $data = [
            'reflection' => new \ReflectionFunction($closure),
            'code'       => null,
            'hasThis'    => false,
            'context'    => [],
            'hasRefs'    => false,
            'binding'    => null,
            'scope'      => null,
            'isStatic'   => $this->isClosureStatic($closure),
        ];

        $this->determineCode($data);
        $this->determineContext($data);
        $this->determineBinding($data);

        return $data;
    }

    abstract protected function determineCode(array &$data);

    /**
     * Returns the variables that are in the "use" clause of the closure.
     *
     * These variables are referred to as the "used variables", "static
     * variables", "closed upon variables", or "context" of the closure.
     *
     * @param array $data
     */
    abstract protected function determineContext(array &$data);

    private function determineBinding(array &$data)
    {
        $data['binding'] = $data['reflection']->getClosureThis();
        if ($scope = $data['reflection']->getClosureScopeClass()) {
            $data['scope'] = $scope->getName();
        }
    }

    private function isClosureStatic(\Closure $closure)
    {
        $closure = @$closure->bindTo(new \stdClass);

        if ($closure === null) {
            return true;
        }

        $rebound = new \ReflectionFunction($closure);

        return $rebound->getClosureThis() === null;
    }
}


<?php namespace SuperClosure;

use Closure;
use SuperClosure\Exception\ClosureUnserializationException;

/**
 * This class acts as a wrapper for a closure, and allows it to be serialized.
 *
 * With the combined power of the Reflection API, code parsing, and the infamous
 * `eval()` function, you can serialize a closure, unserialize it somewhere
 * else (even a different PHP process), and execute it.
 */
class SerializableClosure implements \Serializable
{
    /**
     * The closure being wrapped for serialization.
     *
     * @var Closure
     */
    private $closure;

    /**
     * The serializer doing the serialization work.
     *
     * @var SerializerInterface
     */
    private $serializer;

    /**
     * The data from unserialization.
     *
     * @var array
     */
    private $data;

    /**
     * Create a new serializable closure instance.
     *
     * @param Closure                  $closure
     * @param SerializerInterface|null $serializer
     */
    public function __construct(
        \Closure $closure,
        SerializerInterface $serializer = null
    ) {
        $this->closure = $closure;
        $this->serializer = $serializer ?: new Serializer;
    }

    /**
     * Return the original closure object.
     *
     * @return Closure
     */
    public function getClosure()
    {
        return $this->closure;
    }

    /**
     * Delegates the closure invocation to the actual closure object.
     *
     * Important Notes:
     *
     * - `ReflectionFunction::invokeArgs()` should not be used here, because it
     *   does not work with closure bindings.
     * - Args passed-by-reference lose their references when proxied through
     *   `__invoke()`. This is an unfortunate, but understandable, limitation
     *   of PHP that will probably never change.
     *
     * @return mixed
     */
    public function __invoke()
    {
        return call_user_func_array($this->closure, func_get_args());
    }

    /**
     * Clones the SerializableClosure with a new bound object and class scope.
     *
     * The method is essentially a wrapped proxy to the Closure::bindTo method.
     *
     * @param mixed $newthis  The object to which the closure should be bound,
     *                        or NULL for the closure to be unbound.
     * @param mixed $newscope The class scope to which the closure is to be
     *                        associated, or 'static' to keep the current one.
     *                        If an object is given, the type of the object will
     *                        be used instead. This determines the visibility of
     *                        protected and private methods of the bound object.
     *
     * @return SerializableClosure
     * @link http://www.php.net/manual/en/closure.bindto.php
     */
    public function bindTo($newthis, $newscope = 'static')
    {
        return new self(
            $this->closure->bindTo($newthis, $newscope),
            $this->serializer
        );
    }

    /**
     * Serializes the code, context, and binding of the closure.
     *
     * @return string|null
     * @link http://php.net/manual/en/serializable.serialize.php
     */
    public function serialize()
    {
        try {
            $this->data = $this->data ?: $this->serializer->getData($this->closure, true);
            return serialize($this->data);
        } catch (\Exception $e) {
            trigger_error(
                'Serialization of closure failed: ' . $e->getMessage(),
                E_USER_NOTICE
            );
            // Note: The serialize() method of Serializable must return a string
            // or null and cannot throw exceptions.
            return null;
        }
    }

    /**
     * Unserializes the closure.
     *
     * Unserializes the closure's data and recreates the closure using a
     * simulation of its original context. The used variables (context) are
     * extracted into a fresh scope prior to redefining the closure. The
     * closure is also rebound to its former object and scope.
     *
     * @param string $serialized
     *
     * @throws ClosureUnserializationException
     * @link http://php.net/manual/en/serializable.unserialize.php
     */
    public function unserialize($serialized)
    {
        // Unserialize the closure data and reconstruct the closure object.
        $this->data = unserialize($serialized);
        $this->closure = __reconstruct_closure($this->data);

        // Throw an exception if the closure could not be reconstructed.
        if (!$this->closure instanceof Closure) {
            throw new ClosureUnserializationException(
                'The closure is corrupted and cannot be unserialized.'
            );
        }

        // Rebind the closure to its former binding and scope.
        if ($this->data['binding'] || $this->data['isStatic']) {
            $this->closure = $this->closure->bindTo(
                $this->data['binding'],
                $this->data['scope']
            );
        }
    }

    /**
     * Returns closure data for `var_dump()`.
     *
     * @return array
     */
    public function __debugInfo()
    {
        return $this->data ?: $this->serializer->getData($this->closure, true);
    }
}

/**
 * Reconstruct a closure.
 *
 * HERE BE DRAGONS!
 *
 * The infamous `eval()` is used in this method, along with the error
 * suppression operator, and variable variables (i.e., double dollar signs) to
 * perform the unserialization logic. I'm sorry, world!
 *
 * This is also done inside a plain function instead of a method so that the
 * binding and scope of the closure are null.
 *
 * @param array $__data Unserialized closure data.
 *
 * @return Closure|null
 * @internal
 */
function __reconstruct_closure(array $__data)
{
    // Simulate the original context the closure was created in.
    foreach ($__data['context'] as $__var_name => &$__value) {
        if ($__value instanceof SerializableClosure) {
            // Unbox any SerializableClosures in the context.
            $__value = $__value->getClosure();
        } elseif ($__value === Serializer::RECURSION) {
            // Track recursive references (there should only be one).
            $__recursive_reference = $__var_name;
        }

        // Import the variable into this scope.
        ${$__var_name} = $__value;
    }

    // Evaluate the code to recreate the closure.
    try {
        if (isset($__recursive_reference)) {
            // Special handling for recursive closures.
            @eval("\${$__recursive_reference} = {$__data['code']};");
            $__closure = ${$__recursive_reference};
        } else {
            @eval("\$__closure = {$__data['code']};");
        }
    } catch (\ParseError $e) {
        // Discard the parse error.
    }

    return isset($__closure) ? $__closure : null;
}


<?php

/*
 * This file is part of composer/spdx-licenses.
 *
 * (c) Composer <https://github.com/composer>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Composer\Spdx;

/**
 * The SPDX Licenses Updater scrapes licenses from the spdx website
 * and updates the "res/*.json" file accordingly.
 *
 * The class is used by the update script "bin/update-spdx-licenses".
 */
class SpdxLicensesUpdater
{
    /**
     * @param string $file
     * @param string $url
     */
    public function dumpLicenses($file = null, $url = 'https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json')
    {
        if (null === $file) {
            $file = SpdxLicenses::getResourcesDir() . '/' . SpdxLicenses::LICENSES_FILE;
        }

        $options = 0;

        if (defined('JSON_PRETTY_PRINT')) {
            $options |= JSON_PRETTY_PRINT;
        }

        if (defined('JSON_UNESCAPED_SLASHES')) {
            $options |= JSON_UNESCAPED_SLASHES;
        }

        $licenses = json_encode($this->getLicenses($url), $options);
        file_put_contents($file, $licenses);
    }

    /**
     * @param string $file
     * @param string $url
     */
    public function dumpExceptions($file = null, $url = 'https://raw.githubusercontent.com/spdx/license-list-data/master/json/exceptions.json')
    {
        if (null === $file) {
            $file = SpdxLicenses::getResourcesDir() . '/' . SpdxLicenses::EXCEPTIONS_FILE;
        }

        $options = 0;

        if (defined('JSON_PRETTY_PRINT')) {
            $options |= JSON_PRETTY_PRINT;
        }

        if (defined('JSON_UNESCAPED_SLASHES')) {
            $options |= JSON_UNESCAPED_SLASHES;
        }

        $exceptions = json_encode($this->getExceptions($url), $options);
        file_put_contents($file, $exceptions);
    }

    /**
     * @param string $url
     *
     * @return array
     */
    private function getLicenses($url)
    {
        $licenses = array();

        $data = json_decode(file_get_contents($url), true);

        foreach ($data['licenses'] as $info) {
            $licenses[$info['licenseId']] = array(
                trim($info['name']), $info['isOsiApproved'], $info['isDeprecatedLicenseId']
            );
        }

        uksort($licenses, 'strcasecmp');

        return $licenses;
    }

    /**
     * @param string $url
     *
     * @return array
     */
    private function getExceptions($url)
    {
        $exceptions = array();

        $data = json_decode(file_get_contents($url), true);

        foreach ($data['exceptions'] as $info) {
            $exceptions[$info['licenseExceptionId']] = array(trim($info['name']));
        }

        uksort($exceptions, 'strcasecmp');

        return $exceptions;
    }
}


<?php

/*
 * This file is part of composer/spdx-licenses.
 *
 * (c) Composer <https://github.com/composer>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Composer\Spdx;

class SpdxLicenses
{
    /** @var string */
    const LICENSES_FILE = 'spdx-licenses.json';

    /** @var string */
    const EXCEPTIONS_FILE = 'spdx-exceptions.json';

    /**
     * Contains all the licenses.
     *
     * The array is indexed by license identifiers, which contain
     * a numerically indexed array with license details.
     *
     *  [ lowercased license identifier =>
     *      [ 0 => identifier (string), 1 => full name (string), 2 => osi certified (bool), 3 => deprecated (bool) ]
     *    , ...
     *  ]
     *
     * @var array
     */
    private $licenses;

    /**
     * @var string
     */
    private $licensesExpression;

    /**
     * Contains all the license exceptions.
     *
     * The array is indexed by license exception identifiers, which contain
     * a numerically indexed array with license exception details.
     *
     *  [ lowercased exception identifier =>
     *      [ 0 => exception identifier (string), 1 => full name (string) ]
     *    , ...
     *  ]
     *
     * @var array
     */
    private $exceptions;

    /**
     * @var string
     */
    private $exceptionsExpression;

    public function __construct()
    {
        $this->loadLicenses();
        $this->loadExceptions();
    }

    /**
     * Returns license metadata by license identifier.
     *
     * This function adds a link to the full license text to the license metadata.
     * The array returned is in the form of:
     *
     *  [ 0 => full name (string), 1 => osi certified, 2 => link to license text (string), 3 => deprecation status (bool) ]
     *
     * @param string $identifier
     *
     * @return array|null
     */
    public function getLicenseByIdentifier($identifier)
    {
        $key = strtolower($identifier);

        if (!isset($this->licenses[$key])) {
            return;
        }

        list($identifier, $name, $isOsiApproved, $isDeprecatedLicenseId) = $this->licenses[$key];

        return array(
            $name,
            $isOsiApproved,
            'https://spdx.org/licenses/' . $identifier . '.html#licenseText',
            $isDeprecatedLicenseId,
        );
    }

    /**
     * Returns all licenses information, keyed by the lowercased license identifier.
     *
     * @return array[] Each item is [ 0 => identifier (string), 1 => full name (string), 2 => osi certified (bool), 3 => deprecated (bool) ]
     */
    public function getLicenses()
    {
        return $this->licenses;
    }

    /**
     * Returns license exception metadata by license exception identifier.
     *
     * This function adds a link to the full license exception text to the license exception metadata.
     * The array returned is in the form of:
     *
     *  [ 0 => full name (string), 1 => link to license text (string) ]
     *
     * @param string $identifier
     *
     * @return array|null
     */
    public function getExceptionByIdentifier($identifier)
    {
        $key = strtolower($identifier);

        if (!isset($this->exceptions[$key])) {
            return;
        }

        list($identifier, $name) = $this->exceptions[$key];

        return array(
            $name,
            'https://spdx.org/licenses/' . $identifier . '.html#licenseExceptionText',
        );
    }

    /**
     * Returns the short identifier of a license (or license exception) by full name.
     *
     * @param string $name
     *
     * @return string|null
     */
    public function getIdentifierByName($name)
    {
        foreach ($this->licenses as $licenseData) {
            if ($licenseData[1] === $name) {
                return $licenseData[0];
            }
        }

        foreach ($this->exceptions as $licenseData) {
            if ($licenseData[1] === $name) {
                return $licenseData[0];
            }
        }
    }

    /**
     * Returns the OSI Approved status for a license by identifier.
     *
     * @param string $identifier
     *
     * @return bool
     */
    public function isOsiApprovedByIdentifier($identifier)
    {
        return $this->licenses[strtolower($identifier)][2];
    }

    /**
     * Returns the deprecation status for a license by identifier.
     *
     * @param string $identifier
     *
     * @return bool
     */
    public function isDeprecatedByIdentifier($identifier)
    {
        return $this->licenses[strtolower($identifier)][3];
    }

    /**
     * @param array|string $license
     *
     * @throws \InvalidArgumentException
     *
     * @return bool
     */
    public function validate($license)
    {
        if (is_array($license)) {
            $count = count($license);
            if ($count !== count(array_filter($license, 'is_string'))) {
                throw new \InvalidArgumentException('Array of strings expected.');
            }
            $license = $count > 1  ? '(' . implode(' OR ', $license) . ')' : (string) reset($license);
        }

        if (!is_string($license)) {
            throw new \InvalidArgumentException(sprintf(
                'Array or String expected, %s given.',
                gettype($license)
            ));
        }

        return $this->isValidLicenseString($license);
    }

    /**
     * @return string
     */
    public static function getResourcesDir()
    {
        return dirname(__DIR__) . '/res';
    }

    private function loadLicenses()
    {
        if (null !== $this->licenses) {
            return;
        }

        $json = file_get_contents(self::getResourcesDir() . '/' . self::LICENSES_FILE);
        $this->licenses = array();

        foreach (json_decode($json, true) as $identifier => $license) {
            $this->licenses[strtolower($identifier)] = array($identifier, $license[0], $license[1], $license[2]);
        }
    }

    private function loadExceptions()
    {
        if (null !== $this->exceptions) {
            return;
        }

        $json = file_get_contents(self::getResourcesDir() . '/' . self::EXCEPTIONS_FILE);
        $this->exceptions = array();

        foreach (json_decode($json, true) as $identifier => $exception) {
            $this->exceptions[strtolower($identifier)] = array($identifier, $exception[0]);
        }
    }

    /**
     * @return string
     */
    private function getLicensesExpression()
    {
        if (null === $this->licensesExpression) {
            $licenses = array_map('preg_quote', array_keys($this->licenses));
            rsort($licenses);
            $licenses = implode('|', $licenses);
            $this->licensesExpression = $licenses;
        }

        return $this->licensesExpression;
    }

    /**
     * @return string
     */
    private function getExceptionsExpression()
    {
        if (null === $this->exceptionsExpression) {
            $exceptions = array_map('preg_quote', array_keys($this->exceptions));
            rsort($exceptions);
            $exceptions = implode('|', $exceptions);
            $this->exceptionsExpression = $exceptions;
        }

        return $this->exceptionsExpression;
    }

    /**
     * @param string $license
     *
     * @throws \RuntimeException
     *
     * @return bool
     */
    private function isValidLicenseString($license)
    {
        if (isset($this->licenses[strtolower($license)])) {
            return true;
        }

        $licenses = $this->getLicensesExpression();
        $exceptions = $this->getExceptionsExpression();

        $regex = <<<REGEX
{
(?(DEFINE)
    # idstring: 1*( ALPHA / DIGIT / - / . )
    (?<idstring>[\pL\pN.-]{1,})

    # license-id: taken from list
    (?<licenseid>${licenses})

    # license-exception-id: taken from list
    (?<licenseexceptionid>${exceptions})

    # license-ref: [DocumentRef-1*(idstring):]LicenseRef-1*(idstring)
    (?<licenseref>(?:DocumentRef-(?&idstring):)?LicenseRef-(?&idstring))

    # simple-expresssion: license-id / license-id+ / license-ref
    (?<simple_expression>(?&licenseid)\+? | (?&licenseid) | (?&licenseref))

    # compound-expression: 1*(
    #   simple-expression /
    #   simple-expression WITH license-exception-id /
    #   compound-expression AND compound-expression /
    #   compound-expression OR compound-expression
    # ) / ( compound-expression ) )
    (?<compound_head>
        (?&simple_expression) ( \s+ WITH \s+ (?&licenseexceptionid))?
            | \( \s* (?&compound_expression) \s* \)
    )
    (?<compound_expression>
        (?&compound_head) (?: \s+ (?:AND|OR) \s+ (?&compound_expression))?
    )

    # license-expression: 1*1(simple-expression / compound-expression)
    (?<license_expression>(?&compound_expression) | (?&simple_expression))
) # end of define

^(NONE | NOASSERTION | (?&license_expression))$
}xi
REGEX;

        $match = preg_match($regex, $license);

        if (0 === $match) {
            return false;
        }

        if (false === $match) {
            throw new \RuntimeException('Regex failed to compile/run.');
        }

        return true;
    }
}


<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sensio\Bundle\GeneratorBundle\Command;

use Sensio\Bundle\GeneratorBundle\Manipulator\ConfigurationManipulator;
use Sensio\Bundle\GeneratorBundle\Model\Bundle;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\HttpKernel\KernelInterface;
use Sensio\Bundle\GeneratorBundle\Generator\BundleGenerator;
use Sensio\Bundle\GeneratorBundle\Manipulator\KernelManipulator;
use Sensio\Bundle\GeneratorBundle\Manipulator\RoutingManipulator;

/**
 * Generates bundles.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class GenerateBundleCommand extends GeneratorCommand
{
    /**
     * @see Command
     */
    protected function configure()
    {
        $this
            ->setName('generate:bundle')
            ->setDescription('Generates a bundle')
            ->setDefinition(array(
                new InputOption('namespace', '', InputOption::VALUE_REQUIRED, 'The namespace of the bundle to create'),
                new InputOption('dir', '', InputOption::VALUE_REQUIRED, 'The directory where to create the bundle', 'src/'),
                new InputOption('bundle-name', '', InputOption::VALUE_REQUIRED, 'The optional bundle name'),
                new InputOption('format', '', InputOption::VALUE_REQUIRED, 'Use the format for configuration files (php, xml, yml, or annotation)'),
                new InputOption('shared', '', InputOption::VALUE_NONE, 'Are you planning on sharing this bundle across multiple applications?'),
            ))
            ->setHelp(<<<EOT
The <info>%command.name%</info> command helps you generates new bundles.

By default, the command interacts with the developer to tweak the generation.
Any passed option will be used as a default value for the interaction
(<comment>--namespace</comment> is the only one needed if you follow the
conventions):

<info>php %command.full_name% --namespace=Acme/BlogBundle</info>

Note that you can use <comment>/</comment> instead of <comment>\\ </comment>for the namespace delimiter to avoid any
problems.

If you want to disable any user interaction, use <comment>--no-interaction</comment> but don't forget to pass all needed options:

<info>php %command.full_name% --namespace=Acme/BlogBundle --dir=src [--bundle-name=...] --no-interaction</info>

Note that the bundle namespace must end with "Bundle".
EOT
            )
        ;
    }

    /**
     * @see Command
     *
     * @throws \InvalidArgumentException When namespace doesn't end with Bundle
     * @throws \RuntimeException         When bundle can't be executed
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $questionHelper = $this->getQuestionHelper();

        $bundle = $this->createBundleObject($input);
        $questionHelper->writeSection($output, 'Bundle generation');

        /** @var BundleGenerator $generator */
        $generator = $this->getGenerator();

        $output->writeln(sprintf(
            '> Generating a sample bundle skeleton into <info>%s</info>',
            $this->makePathRelative($bundle->getTargetDirectory())
        ));
        $generator->generateBundle($bundle);

        $errors = array();
        $runner = $questionHelper->getRunner($output, $errors);

        // check that the namespace is already autoloaded
        $runner($this->checkAutoloader($output, $bundle));

        // register the bundle in the Kernel class
        $runner($this->updateKernel($output, $this->getContainer()->get('kernel'), $bundle));

        // routing importing
        $runner($this->updateRouting($output, $bundle));

        if (!$bundle->shouldGenerateDependencyInjectionDirectory()) {
            // we need to import their services.yml manually!
            $runner($this->updateConfiguration($output, $bundle));
        }

        $questionHelper->writeGeneratorSummary($output, $errors);
    }

    protected function interact(InputInterface $input, OutputInterface $output)
    {
        $questionHelper = $this->getQuestionHelper();
        $questionHelper->writeSection($output, 'Welcome to the Symfony bundle generator!');

        /*
         * shared option
         */
        $shared = $input->getOption('shared');
        // ask, but use $shared as the default
        $question = new ConfirmationQuestion($questionHelper->getQuestion(
            'Are you planning on sharing this bundle across multiple applications?',
            $shared ? 'yes' : 'no'
        ), $shared);
        $shared = $questionHelper->ask($input, $output, $question);
        $input->setOption('shared', $shared);

        /*
         * namespace option
         */
        $namespace = $input->getOption('namespace');
        $output->writeln(array(
            '',
            'Your application code must be written in <comment>bundles</comment>. This command helps',
            'you generate them easily.',
            '',
        ));

        $askForBundleName = true;
        if ($shared) {
            // a shared bundle, so it should probably have a vendor namespace
            $output->writeln(array(
                'Each bundle is hosted under a namespace (like <comment>Acme/BlogBundle</comment>).',
                'The namespace should begin with a "vendor" name like your company name, your',
                'project name, or your client name, followed by one or more optional category',
                'sub-namespaces, and it should end with the bundle name itself',
                '(which must have <comment>Bundle</comment> as a suffix).',
                '',
                'See http://symfony.com/doc/current/cookbook/bundles/best_practices.html#bundle-name for more',
                'details on bundle naming conventions.',
                '',
                'Use <comment>/</comment> instead of <comment>\\ </comment>for the namespace delimiter to avoid any problems.',
                '',
            ));

            $question = new Question($questionHelper->getQuestion(
                'Bundle namespace',
                $namespace
            ), $namespace);
            $question->setValidator(function ($answer) {
                return Validators::validateBundleNamespace($answer, true);
            });
            $namespace = $questionHelper->ask($input, $output, $question);
        } else {
            // a simple application bundle
            $output->writeln(array(
                'Give your bundle a descriptive name, like <comment>BlogBundle</comment>.',
            ));

            $question = new Question($questionHelper->getQuestion(
                'Bundle name',
                $namespace
            ), $namespace);
            $question->setValidator(function ($inputNamespace) {
                return Validators::validateBundleNamespace($inputNamespace, false);
            });
            $namespace = $questionHelper->ask($input, $output, $question);

            if (strpos($namespace, '\\') === false) {
                // this is a bundle name (FooBundle) not a namespace (Acme\FooBundle)
                // so this is the bundle name (and it is also the namespace)
                $input->setOption('bundle-name', $namespace);
                $askForBundleName = false;
            }
        }
        $input->setOption('namespace', $namespace);

        /*
         * bundle-name option
         */
        if ($askForBundleName) {
            $bundle = $input->getOption('bundle-name');
            // no bundle yet? Get a default from the namespace
            if (!$bundle) {
                $bundle = strtr($namespace, array('\\Bundle\\' => '', '\\' => ''));
            }

            $output->writeln(array(
                '',
                'In your code, a bundle is often referenced by its name. It can be the',
                'concatenation of all namespace parts but it\'s really up to you to come',
                'up with a unique name (a good practice is to start with the vendor name).',
                'Based on the namespace, we suggest <comment>'.$bundle.'</comment>.',
                '',
            ));
            $question = new Question($questionHelper->getQuestion(
                'Bundle name',
                $bundle
            ), $bundle);
            $question->setValidator(
                array('Sensio\Bundle\GeneratorBundle\Command\Validators', 'validateBundleName')
            );
            $bundle = $questionHelper->ask($input, $output, $question);
            $input->setOption('bundle-name', $bundle);
        }

        /*
         * dir option
         */
        // defaults to src/ in the option
        $dir = $input->getOption('dir');
        $output->writeln(array(
            '',
            'Bundles are usually generated into the <info>src/</info> directory. Unless you\'re',
            'doing something custom, hit enter to keep this default!',
            '',
        ));

        $question = new Question($questionHelper->getQuestion(
            'Target Directory',
            $dir
        ), $dir);
        $dir = $questionHelper->ask($input, $output, $question);
        $input->setOption('dir', $dir);

        /*
         * format option
         */
        $format = $input->getOption('format');
        if (!$format) {
            $format = $shared ? 'xml' : 'annotation';
        }
        $output->writeln(array(
            '',
            'What format do you want to use for your generated configuration?',
            '',
        ));

        $question = new Question($questionHelper->getQuestion(
            'Configuration format (annotation, yml, xml, php)',
            $format
        ), $format);
        $question->setValidator(function ($format) {
            return Validators::validateFormat($format);
        });
        $question->setAutocompleterValues(array('annotation', 'yml', 'xml', 'php'));
        $format = $questionHelper->ask($input, $output, $question);
        $input->setOption('format', $format);
    }

    protected function checkAutoloader(OutputInterface $output, Bundle $bundle)
    {
        $output->writeln('> Checking that the bundle is autoloaded');
        if (!class_exists($bundle->getBundleClassName())) {
            return array(
                '- Edit the <comment>composer.json</comment> file and register the bundle',
                '  namespace in the "autoload" section:',
                '',
            );
        }
    }

    protected function updateKernel(OutputInterface $output, KernelInterface $kernel, Bundle $bundle)
    {
        $kernelManipulator = new KernelManipulator($kernel);

        $output->writeln(sprintf(
            '> Enabling the bundle inside <info>%s</info>',
            $this->makePathRelative($kernelManipulator->getFilename())
        ));

        try {
            $ret = $kernelManipulator->addBundle($bundle->getBundleClassName());

            if (!$ret) {
                $reflected = new \ReflectionObject($kernel);

                return array(
                    sprintf('- Edit <comment>%s</comment>', $reflected->getFilename()),
                    '  and add the following bundle in the <comment>AppKernel::registerBundles()</comment> method:',
                    '',
                    sprintf('    <comment>new %s(),</comment>', $bundle->getBundleClassName()),
                    '',
                );
            }
        } catch (\RuntimeException $e) {
            return array(
                sprintf('Bundle <comment>%s</comment> is already defined in <comment>AppKernel::registerBundles()</comment>.', $bundle->getBundleClassName()),
                '',
            );
        }
    }

    protected function updateRouting(OutputInterface $output, Bundle $bundle)
    {
        $targetRoutingPath = $this->getContainer()->getParameter('kernel.root_dir').'/config/routing.yml';
        $output->writeln(sprintf(
            '> Importing the bundle\'s routes from the <info>%s</info> file',
            $this->makePathRelative($targetRoutingPath)
        ));
        $routing = new RoutingManipulator($targetRoutingPath);
        try {
            $ret = $routing->addResource($bundle->getName(), $bundle->getConfigurationFormat());
            if (!$ret) {
                if ('annotation' === $bundle->getConfigurationFormat()) {
                    $help = sprintf("        <comment>resource: \"@%s/Controller/\"</comment>\n        <comment>type:     annotation</comment>\n", $bundle->getName());
                } else {
                    $help = sprintf("        <comment>resource: \"@%s/Resources/config/routing.%s\"</comment>\n", $bundle->getName(), $bundle->getConfigurationFormat());
                }
                $help .= "        <comment>prefix:   /</comment>\n";

                return array(
                    '- Import the bundle\'s routing resource in the app\'s main routing file:',
                    '',
                    sprintf('    <comment>%s:</comment>', $bundle->getName()),
                    $help,
                    '',
                );
            }
        } catch (\RuntimeException $e) {
            return array(
                sprintf('Bundle <comment>%s</comment> is already imported.', $bundle->getName()),
                '',
            );
        }
    }

    protected function updateConfiguration(OutputInterface $output, Bundle $bundle)
    {
        $targetConfigurationPath = $this->getContainer()->getParameter('kernel.root_dir').'/config/config.yml';
        $output->writeln(sprintf(
            '> Importing the bundle\'s %s from the <info>%s</info> file',
            $bundle->getServicesConfigurationFilename(),
            $this->makePathRelative($targetConfigurationPath)
        ));
        $manipulator = new ConfigurationManipulator($targetConfigurationPath);
        try {
            $manipulator->addResource($bundle);
        } catch (\RuntimeException $e) {
            return array(
                sprintf('- Import the bundle\'s "%s" resource in the app\'s main configuration file:', $bundle->getServicesConfigurationFilename()),
                '',
                $manipulator->getImportCode($bundle),
                '',
            );
        }
    }

    /**
     * Creates the Bundle object based on the user's (non-interactive) input.
     *
     * @param InputInterface $input
     *
     * @return Bundle
     */
    protected function createBundleObject(InputInterface $input)
    {
        foreach (array('namespace', 'dir') as $option) {
            if (null === $input->getOption($option)) {
                throw new \RuntimeException(sprintf('The "%s" option must be provided.', $option));
            }
        }

        $shared = $input->getOption('shared');

        $namespace = Validators::validateBundleNamespace($input->getOption('namespace'), $shared);
        if (!$bundleName = $input->getOption('bundle-name')) {
            $bundleName = strtr($namespace, array('\\' => ''));
        }
        $bundleName = Validators::validateBundleName($bundleName);
        $dir = $input->getOption('dir');
        if (null === $input->getOption('format')) {
            $input->setOption('format', 'annotation');
        }
        $format = Validators::validateFormat($input->getOption('format'));

        // an assumption that the kernel root dir is in a directory (like app/)
        $projectRootDirectory = $this->getContainer()->getParameter('kernel.root_dir').'/..';

        if (!$this->getContainer()->get('filesystem')->isAbsolutePath($dir)) {
            $dir = $projectRootDirectory.'/'.$dir;
        }
        // add trailing / if necessary
        $dir = '/' === substr($dir, -1, 1) ? $dir : $dir.'/';

        $bundle = new Bundle(
            $namespace,
            $bundleName,
            $dir,
            $format,
            $shared
        );

        // not shared - put the tests in the root
        if (!$shared) {
            $testsDir = $projectRootDirectory.'/tests/'.$bundleName;
            $bundle->setTestsDirectory($testsDir);
        }

        return $bundle;
    }

    protected function createGenerator()
    {
        return new BundleGenerator($this->getContainer()->get('filesystem'));
    }
}


<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sensio\Bundle\GeneratorBundle\Manipulator;

use Sensio\Bundle\GeneratorBundle\Generator\Generator;
use Sensio\Bundle\GeneratorBundle\Model\Bundle;
use Symfony\Component\Yaml\Yaml;

/**
 * Changes the PHP code of a YAML services configuration file.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Ryan Weaver <weaverryan@gmail.com>
 */
class ConfigurationManipulator extends Manipulator
{
    private $file;

    /**
     * @param string $file The YAML configuration file path
     */
    public function __construct($file)
    {
        $this->file = $file;
    }

    /**
     * Adds a configuration resource at the top of the existing ones.
     *
     * @param Bundle $bundle
     *
     * @throws \RuntimeException If this process fails for any reason
     */
    public function addResource(Bundle $bundle)
    {
        // if the config.yml file doesn't exist, don't even try.
        if (!file_exists($this->file)) {
            throw new \RuntimeException(sprintf('The target config file %s does not exist', $this->file));
        }

        $code = $this->getImportCode($bundle);

        $currentContents = file_get_contents($this->file);
        // Don't add same bundle twice
        if (false !== strpos($currentContents, $code)) {
            throw new \RuntimeException(sprintf('The %s configuration file from %s is already imported', $bundle->getServicesConfigurationFilename(), $bundle->getName()));
        }

        // find the "imports" line and add this at the end of that list
        $lastImportedPath = $this->findLastImportedPath($currentContents);
        if (!$lastImportedPath) {
            throw new \RuntimeException(sprintf('Could not find the imports key in %s', $this->file));
        }

        // find imports:
        $importsPosition = strpos($currentContents, 'imports:');
        // find the last import
        $lastImportPosition = strpos($currentContents, $lastImportedPath, $importsPosition);
        // find the line break after the last import
        $targetLinebreakPosition = strpos($currentContents, "\n", $lastImportPosition);

        $newContents = substr($currentContents, 0, $targetLinebreakPosition)."\n".$code.substr($currentContents, $targetLinebreakPosition);

        if (false === Generator::dump($this->file, $newContents)) {
            throw new \RuntimeException(sprintf('Could not write file %s ', $this->file));
        }
    }

    public function getImportCode(Bundle $bundle)
    {
        return sprintf(<<<EOF
    - { resource: "@%s/Resources/config/%s" }
EOF
        ,
            $bundle->getName(),
            $bundle->getServicesConfigurationFilename()
        );
    }

    /**
     * Finds the last imported resource path in the YAML file.
     *
     * @param $yamlContents
     *
     * @return bool|string
     */
    private function findLastImportedPath($yamlContents)
    {
        $data = Yaml::parse($yamlContents);
        if (!isset($data['imports'])) {
            return false;
        }

        // find the last imports entry
        $lastImport = end($data['imports']);
        if (!isset($lastImport['resource'])) {
            return false;
        }

        return $lastImport['resource'];
    }
}