<?php

namespace Arrilot\BitrixModels\Models;

use Arrilot\BitrixModels\Exceptions\ExceptionFromBitrix;
use Arrilot\BitrixModels\Queries\SectionQuery;
use CIBlock;
use Illuminate\Support\Collection;
use LogicException;

/**
 * SectionQuery methods
 * @method static static getByCode(string $code)
 * @method static static getByExternalId(string $id)
 * @method static SectionQuery countElements($value)
 *
 * Base Query methods
 * @method static Collection|static[] getList()
 * @method static static first()
 * @method static static getById(int $id)
 * @method static SectionQuery sort(string|array $by, string $order='ASC')
 * @method static SectionQuery order(string|array $by, string $order='ASC') // same as sort()
 * @method static SectionQuery filter(array $filter)
 * @method static SectionQuery addFilter(array $filters)
 * @method static SectionQuery resetFilter()
 * @method static SectionQuery navigation(array $filter)
 * @method static SectionQuery select($value)
 * @method static SectionQuery keyBy(string $value)
 * @method static SectionQuery limit(int $value)
 * @method static SectionQuery offset(int $value)
 * @method static SectionQuery page(int $num)
 * @method static SectionQuery take(int $value) // same as limit()
 * @method static SectionQuery forPage(int $page, int $perPage=15)
 * @method static \Illuminate\Pagination\LengthAwarePaginator paginate(int $perPage = 15, string $pageName = 'page')
 * @method static \Illuminate\Pagination\Paginator simplePaginate(int $perPage = 15, string $pageName = 'page')
 * @method static SectionQuery stopQuery()
 * @method static SectionQuery cache(float|int $minutes)
 *
 * Scopes
 * @method static SectionQuery active()
 * @method static SectionQuery childrenOf(SectionModel $section)
 * @method static SectionQuery directChildrenOf(SectionModel|int $section)
 */
class SectionModel extends BitrixModel
{
    /**
     * Corresponding IBLOCK_ID
     *
     * @var int
     */
    const IBLOCK_ID = null;

    /**
     * Bitrix entity object.
     *
     * @var object
     */
    public static $bxObject;

    /**
     * Corresponding object class name.
     *
     * @var string
     */
    protected static $objectClass = 'CIBlockSection';

    /**
     * Recalculate LEFT_MARGIN and RIGHT_MARGIN during add/update ($bResort for CIBlockSection::Add/Update).
     *
     * @var bool
     */
    protected static $resort = true;

    /**
     * Update search after each create or update.
     *
     * @var bool
     */
    protected static $updateSearch = true;

    /**
     * Resize pictures during add/update ($bResizePictures for CIBlockSection::Add/Update).
     *
     * @var bool
     */
    protected static $resizePictures = false;

    /**
     * Getter for corresponding iblock id.
     *
     * @throws LogicException
     *
     * @return int
     */
    public static function iblockId()
    {
        $id = static::IBLOCK_ID;
        if (!$id) {
            throw new LogicException('You must set $iblockId property or override iblockId() method');
        }

        return $id;
    }

    /**
     * Instantiate a query object for the model.
     *
     * @return SectionQuery
     */
    public static function query()
    {
        return new SectionQuery(static::instantiateObject(), get_called_class());
    }

    /**
     * Create new item in database.
     *
     * @param $fields
     *
     * @throws ExceptionFromBitrix
     *
     * @return static|bool
     */
    public static function create($fields)
    {
        if (!isset($fields['IBLOCK_ID'])) {
            $fields['IBLOCK_ID'] = static::iblockId();
        }

        return static::internalCreate($fields);
    }

    /**
     * Get IDs of direct children of the section.
     * Additional filter can be specified.
     *
     * @param array $filter
     *
     * @return array
     */
    public function getDirectChildren(array $filter = [])
    {
        return static::query()
            ->filter($filter)
            ->filter(['SECTION_ID' => $this->id])
            ->select('ID')
            ->getList()
            ->transform(function ($section) {
                return (int) $section['ID'];
            })
            ->all();
    }

    /**
     * Get IDs of all children of the section (direct or not).
     * Additional filter can be specified.
     *
     * @param array $filter
     * @param array|string $sort
     *
     * @return array
     */
    public function getAllChildren(array $filter = [], $sort = ['LEFT_MARGIN' => 'ASC'])
    {
        if (!isset($this->fields['LEFT_MARGIN']) || !isset($this->fields['RIGHT_MARGIN'])) {
            $this->refresh();
        }

        return static::query()
            ->sort($sort)
            ->filter($filter)
            ->filter([
                '!ID' => $this->id,
                '>LEFT_MARGIN' => $this->fields['LEFT_MARGIN'],
                '<RIGHT_MARGIN' => $this->fields['RIGHT_MARGIN'],
            ])
            ->select('ID')
            ->getList()
            ->transform(function ($section) {
                return (int) $section['ID'];
            })
            ->all();
    }

    /**
     * Proxy for GetPanelButtons
     *
     * @param array $options
     * @return array
     */
    public function getPanelButtons($options = [])
    {
        return CIBlock::GetPanelButtons(
            static::iblockId(),
            0,
            $this->id,
            $options
        );
    }

    public static function internalDirectCreate($bxObject, $fields)
    {
        return $bxObject->add($fields, static::$resort, static::$updateSearch, static::$resizePictures);
    }

    /**
     * @param $fields
     * @param $fieldsSelectedForSave
     * @return bool
     */
    protected function internalUpdate($fields, $fieldsSelectedForSave)
    {
        return !empty($fields) ? static::$bxObject->update($this->id, $fields, static::$resort, static::$updateSearch, static::$resizePictures) : false;
    }

    /**
     * @param $value
     */
    public static function setResort($value)
    {
        static::$resort = $value;
    }

    /**
     * @param $value
     */
    public static function setUpdateSearch($value)
    {
        static::$updateSearch = $value;
    }

    /**
     * @param $value
     */
    public static function setResizePictures($value)
    {
        static::$resizePictures = $value;
    }

    /**
     * @param $query
     * @param SectionModel $section
     * @return SectionQuery
     */
    public function scopeChildrenOf(SectionQuery $query, SectionModel $section)
    {
        $query->filter['>LEFT_MARGIN'] = $section->fields['LEFT_MARGIN'];
        $query->filter['<RIGHT_MARGIN'] = $section->fields['RIGHT_MARGIN'];

        return $query;
    }

    /**
     * @param $query
     * @param SectionModel|int $section
     * @return SectionQuery
     */
    public function scopeDirectChildrenOf(SectionQuery $query, $section)
    {
        $query->filter['SECTION_ID'] = is_int($section) ? $section : $section->id;

        return $query;
    }
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

use Prooph\EventSourcing\AggregateChanged;

trait EventProducerTrait
{
    /**
     * Current version
     *
     * @var int
     */
    protected $version = 0;

    /**
     * List of events that are not committed to the EventStore
     *
     * @var AggregateChanged[]
     */
    protected $recordedEvents = [];

    /**
     * Get pending events and reset stack
     *
     * @return AggregateChanged[]
     */
    protected function popRecordedEvents(): array
    {
        $pendingEvents = $this->recordedEvents;

        $this->recordedEvents = [];

        return $pendingEvents;
    }

    /**
     * Record an aggregate changed event
     */
    protected function recordThat(AggregateChanged $event): void
    {
        $this->version += 1;

        $this->recordedEvents[] = $event->withVersion($this->version);

        $this->apply($event);
    }

    abstract protected function aggregateId(): string;

    /**
     * Apply given event
     */
    abstract protected function apply(AggregateChanged $event): void;
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

use ArrayIterator;
use Prooph\Common\Messaging\Message;
use Prooph\EventStore\EventStore;
use Prooph\EventStore\Exception\StreamNotFound;
use Prooph\EventStore\Metadata\MetadataMatcher;
use Prooph\EventStore\Metadata\Operator;
use Prooph\EventStore\Stream;
use Prooph\EventStore\StreamName;
use Prooph\SnapshotStore\SnapshotStore;

class AggregateRepository
{
    /**
     * @var EventStore
     */
    protected $eventStore;

    /**
     * @var AggregateTranslator
     */
    protected $aggregateTranslator;

    /**
     * @var AggregateType
     */
    protected $aggregateType;

    /**
     * @var array
     */
    protected $identityMap = [];

    /**
     * @var SnapshotStore|null
     */
    protected $snapshotStore;

    /**
     * @var StreamName
     */
    protected $streamName;

    /**
     * @var bool
     */
    protected $oneStreamPerAggregate;

    /**
     * @var array
     */
    protected $metadata;

    /**
     * @var bool
     */
    protected $disableIdentityMap;

    public function __construct(
        EventStore $eventStore,
        AggregateType $aggregateType,
        AggregateTranslator $aggregateTranslator,
        SnapshotStore $snapshotStore = null,
        StreamName $streamName = null,
        bool $oneStreamPerAggregate = false,
        bool $disableIdentityMap = false,
        array $metadata = []
    ) {
        $this->eventStore = $eventStore;
        $this->aggregateType = $aggregateType;
        $this->aggregateTranslator = $aggregateTranslator;
        $this->snapshotStore = $snapshotStore;
        $this->streamName = $streamName;
        $this->oneStreamPerAggregate = $oneStreamPerAggregate;
        $this->disableIdentityMap = $disableIdentityMap;
        $this->metadata = $metadata;
    }

    /**
     * @param object $eventSourcedAggregateRoot
     */
    public function saveAggregateRoot($eventSourcedAggregateRoot): void
    {
        $this->assertAggregateType($eventSourcedAggregateRoot);

        $domainEvents = $this->aggregateTranslator->extractPendingStreamEvents($eventSourcedAggregateRoot);

        $aggregateId = $this->aggregateTranslator->extractAggregateId($eventSourcedAggregateRoot);

        $streamName = $this->determineStreamName($aggregateId);

        $enrichedEvents = [];

        $createStream = false;

        $firstEvent = \reset($domainEvents);

        if (false === $firstEvent) {
            return;
        }

        if ($this->oneStreamPerAggregate && $this->isFirstEvent($firstEvent)) {
            $createStream = true;
        }

        foreach ($domainEvents as $event) {
            $enrichedEvents[] = $this->enrichEventMetadata($event, $aggregateId);
        }

        if ($createStream) {
            $stream = new Stream($streamName, new ArrayIterator($enrichedEvents), $this->metadata);

            $this->eventStore->create($stream);
        } else {
            $this->eventStore->appendTo($streamName, new ArrayIterator($enrichedEvents));
        }

        if (! $this->disableIdentityMap && isset($this->identityMap[$aggregateId])) {
            unset($this->identityMap[$aggregateId]);
        }
    }

    /**
     * Returns null if no stream events can be found for aggregate root otherwise the reconstituted aggregate root
     *
     * @return null|object
     */
    public function getAggregateRoot(string $aggregateId)
    {
        if (! $this->disableIdentityMap && isset($this->identityMap[$aggregateId])) {
            return $this->identityMap[$aggregateId];
        }

        if ($this->snapshotStore) {
            $eventSourcedAggregateRoot = $this->loadFromSnapshotStore($aggregateId);

            if ($eventSourcedAggregateRoot && ! $this->disableIdentityMap) {
                //Cache aggregate root in the identity map
                $this->identityMap[$aggregateId] = $eventSourcedAggregateRoot;
            }

            return $eventSourcedAggregateRoot;
        }

        $streamName = $this->determineStreamName($aggregateId);

        if ($this->oneStreamPerAggregate) {
            try {
                $streamEvents = $this->eventStore->load($streamName, 1);
            } catch (StreamNotFound $e) {
                return null;
            }
        } else {
            $metadataMatcher = new MetadataMatcher();
            $metadataMatcher = $metadataMatcher->withMetadataMatch(
                '_aggregate_type',
                Operator::EQUALS(),
                $this->aggregateType->toString()
            );
            $metadataMatcher = $metadataMatcher->withMetadataMatch(
                '_aggregate_id',
                Operator::EQUALS(),
                $aggregateId
            );

            try {
                $streamEvents = $this->eventStore->load($streamName, 1, null, $metadataMatcher);
            } catch (StreamNotFound $e) {
                return null;
            }
        }

        if (! $streamEvents->valid()) {
            return null;
        }

        $eventSourcedAggregateRoot = $this->aggregateTranslator->reconstituteAggregateFromHistory(
            $this->aggregateType,
            $streamEvents
        );

        if (! $this->disableIdentityMap) {
            //Cache aggregate root in the identity map but without pending events
            $this->identityMap[$aggregateId] = $eventSourcedAggregateRoot;
        }

        return $eventSourcedAggregateRoot;
    }

    /**
     * @param object $aggregateRoot
     */
    public function extractAggregateVersion($aggregateRoot): int
    {
        return $this->aggregateTranslator->extractAggregateVersion($aggregateRoot);
    }

    /**
     * Empties the identity map. Use this if you load thousands of aggregates to free memory e.g. modulo 500.
     */
    public function clearIdentityMap(): void
    {
        $this->identityMap = [];
    }

    protected function isFirstEvent(Message $message): bool
    {
        return 1 === $message->metadata()['_aggregate_version'];
    }

    /**
     * @return null|object
     */
    protected function loadFromSnapshotStore(string $aggregateId)
    {
        $snapshot = $this->snapshotStore->get($this->aggregateType->toString(), $aggregateId);

        if ($snapshot) {
            $lastVersion = $snapshot->lastVersion();
            $aggregateRoot = $snapshot->aggregateRoot();
        } else {
            $lastVersion = 0;
            $aggregateRoot = null;
        }

        $streamName = $this->determineStreamName($aggregateId);

        if ($this->oneStreamPerAggregate) {
            try {
                $streamEvents = $this->eventStore->load(
                    $streamName,
                    $lastVersion + 1
                );
            } catch (StreamNotFound $e) {
                return $aggregateRoot;
            }
        } else {
            $metadataMatcher = new MetadataMatcher();
            $metadataMatcher = $metadataMatcher->withMetadataMatch(
                '_aggregate_type',
                Operator::EQUALS(),
                $this->aggregateType->toString()
            );
            $metadataMatcher = $metadataMatcher->withMetadataMatch(
                '_aggregate_id',
                Operator::EQUALS(),
                $aggregateId
            );
            $metadataMatcher = $metadataMatcher->withMetadataMatch(
                '_aggregate_version',
                Operator::GREATER_THAN(),
                $lastVersion
            );

            try {
                $streamEvents = $this->eventStore->load(
                    $streamName,
                    $lastVersion + 1,
                    null,
                    $metadataMatcher
                );
            } catch (StreamNotFound $e) {
                return $aggregateRoot;
            }
        }

        if (! $streamEvents->valid()) {
            return $aggregateRoot;
        }

        if ($aggregateRoot) {
            $this->aggregateTranslator->replayStreamEvents($aggregateRoot, $streamEvents);
        } else {
            $aggregateRoot = $this->aggregateTranslator->reconstituteAggregateFromHistory(
                $this->aggregateType,
                $streamEvents
            );
        }

        return $aggregateRoot;
    }

    /**
     * Default stream name generation.
     * Override this method in an extending repository to provide a custom name
     */
    protected function determineStreamName(string $aggregateId): StreamName
    {
        if ($this->oneStreamPerAggregate) {
            if (null === $this->streamName) {
                $prefix = $this->aggregateType->toString();
            } else {
                $prefix = $this->streamName->toString();
            }

            return new StreamName($prefix . '-' . $aggregateId);
        }

        if (null === $this->streamName) {
            return new StreamName('event_stream');
        }

        return $this->streamName;
    }

    /**
     * Add aggregate_id and aggregate_type as metadata to $domainEvent
     * Override this method in an extending repository to add more or different metadata.
     */
    protected function enrichEventMetadata(Message $domainEvent, string $aggregateId): Message
    {
        $domainEvent = $domainEvent->withAddedMetadata('_aggregate_id', $aggregateId);
        $domainEvent = $domainEvent->withAddedMetadata('_aggregate_type', $this->aggregateType->toString());

        return $domainEvent;
    }

    /**
     * @param object $eventSourcedAggregateRoot
     */
    protected function assertAggregateType($eventSourcedAggregateRoot)
    {
        $this->aggregateType->assert($eventSourcedAggregateRoot);
    }
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\EventStoreIntegration;

use Iterator;
use Prooph\Common\Messaging\Message;
use Prooph\EventSourcing\Aggregate\AggregateTranslator as EventStoreAggregateTranslator;
use Prooph\EventSourcing\Aggregate\AggregateType;
use RuntimeException;

final class ClosureAggregateTranslator implements EventStoreAggregateTranslator
{
    protected $aggregateIdExtractor;
    protected $aggregateReconstructor;
    protected $pendingEventsExtractor;
    protected $replayStreamEvents;
    protected $versionExtractor;

    /**
     * @param object $eventSourcedAggregateRoot
     *
     * @return int
     */
    public function extractAggregateVersion($eventSourcedAggregateRoot): int
    {
        if (null === $this->versionExtractor) {
            $this->versionExtractor = function (): int {
                return $this->version;
            };
        }

        return $this->versionExtractor->call($eventSourcedAggregateRoot);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     *
     * @return string
     */
    public function extractAggregateId($anEventSourcedAggregateRoot): string
    {
        if (null === $this->aggregateIdExtractor) {
            $this->aggregateIdExtractor = function (): string {
                return $this->aggregateId();
            };
        }

        return $this->aggregateIdExtractor->call($anEventSourcedAggregateRoot);
    }

    /**
     * @param AggregateType $aggregateType
     * @param Iterator $historyEvents
     *
     * @return object reconstructed AggregateRoot
     */
    public function reconstituteAggregateFromHistory(AggregateType $aggregateType, Iterator $historyEvents)
    {
        if (null === $this->aggregateReconstructor) {
            $this->aggregateReconstructor = function ($historyEvents) {
                return static::reconstituteFromHistory($historyEvents);
            };
        }

        $arClass = $aggregateType->toString();

        if (! \class_exists($arClass)) {
            throw new RuntimeException(
                \sprintf('Aggregate root class %s cannot be found', $arClass)
            );
        }

        return ($this->aggregateReconstructor->bindTo(null, $arClass))($historyEvents);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     *
     * @return Message[]
     */
    public function extractPendingStreamEvents($anEventSourcedAggregateRoot): array
    {
        if (null === $this->pendingEventsExtractor) {
            $this->pendingEventsExtractor = function (): array {
                return $this->popRecordedEvents();
            };
        }

        return $this->pendingEventsExtractor->call($anEventSourcedAggregateRoot);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     * @param Iterator $events
     *
     * @return void
     */
    public function replayStreamEvents($anEventSourcedAggregateRoot, Iterator $events): void
    {
        if (null === $this->replayStreamEvents) {
            $this->replayStreamEvents = function ($events): void {
                $this->replay($events);
            };
        }
        $this->replayStreamEvents->call($anEventSourcedAggregateRoot, $events);
    }
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

class AggregateType
{
    /**
     * @var string|null
     */
    protected $aggregateType;

    /**
     * @var array
     */
    protected $mapping = [];

    /**
     * Use this factory when aggregate type should be detected based on given aggregate root
     *
     * @param object $eventSourcedAggregateRoot
     *
     * @throws Exception\AggregateTypeException
     */
    public static function fromAggregateRoot($eventSourcedAggregateRoot): AggregateType
    {
        if (! \is_object($eventSourcedAggregateRoot)) {
            throw new Exception\AggregateTypeException(
                \sprintf('Aggregate root must be an object but type of %s given', \gettype($eventSourcedAggregateRoot))
            );
        }

        if ($eventSourcedAggregateRoot instanceof AggregateTypeProvider) {
            return $eventSourcedAggregateRoot->aggregateType();
        }

        $self = new static();
        $self->aggregateType = \get_class($eventSourcedAggregateRoot);

        return $self;
    }

    /**
     * Use this factory when aggregate type equals to aggregate root class
     * The factory makes sure that the aggregate root class exists.
     *
     * @throws Exception\InvalidArgumentException
     */
    public static function fromAggregateRootClass(string $aggregateRootClass): AggregateType
    {
        if (! \class_exists($aggregateRootClass)) {
            throw new Exception\InvalidArgumentException(\sprintf('Aggregate root class %s can not be found', $aggregateRootClass));
        }

        $self = new static();
        $self->aggregateType = $aggregateRootClass;

        return $self;
    }

    /**
     * Use this factory when the aggregate type is not equal to the aggregate root class
     *
     * @throws Exception\InvalidArgumentException
     */
    public static function fromString(string $aggregateTypeString): AggregateType
    {
        if (empty($aggregateTypeString)) {
            throw new Exception\InvalidArgumentException('AggregateType must be a non empty string');
        }

        $self = new static();
        $self->aggregateType = $aggregateTypeString;

        return $self;
    }

    public static function fromMapping(array $mapping): AggregateType
    {
        $self = new static();
        $self->mapping = $mapping;

        return $self;
    }

    private function __construct()
    {
    }

    public function mappedClass(): ?string
    {
        return empty($this->mapping) ? null : \current($this->mapping);
    }

    public function toString(): string
    {
        return empty($this->mapping) ? $this->aggregateType : \key($this->mapping);
    }

    public function __toString(): string
    {
        return $this->toString();
    }

    /**
     * @param object $aggregateRoot
     *
     * @throws Exception\AggregateTypeException
     */
    public function assert($aggregateRoot): void
    {
        $otherAggregateType = self::fromAggregateRoot($aggregateRoot);

        if (! $this->equals($otherAggregateType)) {
            throw new Exception\AggregateTypeException(
                \sprintf('Aggregate types must be equal. %s != %s', $this->toString(), $otherAggregateType->toString())
            );
        }
    }

    public function equals(AggregateType $other): bool
    {
        if (! $aggregateTypeString = $this->mappedClass()) {
            $aggregateTypeString = $this->toString();
        }

        return $aggregateTypeString === $other->toString() || $aggregateTypeString === $other->mappedClass();
    }
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\EventStoreIntegration;

use Iterator;
use Prooph\Common\Messaging\Message;
use Prooph\EventSourcing\Aggregate\AggregateTranslator as EventStoreAggregateTranslator;
use Prooph\EventSourcing\Aggregate\AggregateType;

final class AggregateTranslator implements EventStoreAggregateTranslator
{
    /**
     * @var AggregateRootDecorator
     */
    protected $aggregateRootDecorator;

    /**
     * @param object $eventSourcedAggregateRoot
     *
     * @return int
     */
    public function extractAggregateVersion($eventSourcedAggregateRoot): int
    {
        return $this->getAggregateRootDecorator()->extractAggregateVersion($eventSourcedAggregateRoot);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     *
     * @return string
     */
    public function extractAggregateId($anEventSourcedAggregateRoot): string
    {
        return $this->getAggregateRootDecorator()->extractAggregateId($anEventSourcedAggregateRoot);
    }

    /**
     * @param AggregateType $aggregateType
     * @param Iterator $historyEvents
     *
     * @return object reconstructed AggregateRoot
     */
    public function reconstituteAggregateFromHistory(AggregateType $aggregateType, Iterator $historyEvents)
    {
        if (! $aggregateRootClass = $aggregateType->mappedClass()) {
            $aggregateRootClass = $aggregateType->toString();
        }

        return $this->getAggregateRootDecorator()
            ->fromHistory($aggregateRootClass, $historyEvents);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     *
     * @return Message[]
     */
    public function extractPendingStreamEvents($anEventSourcedAggregateRoot): array
    {
        return $this->getAggregateRootDecorator()->extractRecordedEvents($anEventSourcedAggregateRoot);
    }

    /**
     * @param object $anEventSourcedAggregateRoot
     * @param Iterator $events
     *
     * @return void
     */
    public function replayStreamEvents($anEventSourcedAggregateRoot, Iterator $events): void
    {
        $this->getAggregateRootDecorator()->replayStreamEvents($anEventSourcedAggregateRoot, $events);
    }

    public function getAggregateRootDecorator(): AggregateRootDecorator
    {
        if (null === $this->aggregateRootDecorator) {
            $this->aggregateRootDecorator = AggregateRootDecorator::newInstance();
        }

        return $this->aggregateRootDecorator;
    }

    public function setAggregateRootDecorator(AggregateRootDecorator $anAggregateRootDecorator): void
    {
        $this->aggregateRootDecorator = $anAggregateRootDecorator;
    }
}


<?php

/**
 * This file is part of prooph/event-sourcing.
 * (c) 2014-2018 prooph software GmbH <contact@prooph.de>
 * (c) 2015-2018 Sascha-Oliver Prolic <saschaprolic@googlemail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Prooph\EventSourcing\Aggregate;

use Iterator;
use Prooph\EventSourcing\AggregateChanged;
use RuntimeException;

trait EventSourcedTrait
{
    /**
     * Current version
     *
     * @var int
     */
    protected $version = 0;

    /**
     * @throws RuntimeException
     */
    protected static function reconstituteFromHistory(Iterator $historyEvents): self
    {
        $instance = new static();
        $instance->replay($historyEvents);

        return $instance;
    }

    /**
     * Replay past events
     *
     * @throws RuntimeException
     */
    protected function replay(Iterator $historyEvents): void
    {
        foreach ($historyEvents as $pastEvent) {
            /** @var AggregateChanged $pastEvent */
            $this->version = $pastEvent->version();

            $this->apply($pastEvent);
        }
    }

    abstract protected function aggregateId(): string;

    /**
     * Apply given event
     */
    abstract protected function apply(AggregateChanged $event): void;
}


<?php declare(strict_types=1);

namespace Igni\Application;

use Igni\Application\Exception\ApplicationException;
use Igni\Application\Http\MiddlewareAggregator;
use Igni\Application\Listeners\OnBootListener;
use Igni\Application\Listeners\OnErrorListener;
use Igni\Application\Listeners\OnRunListener;
use Igni\Application\Listeners\OnShutDownListener;
use Igni\Application\Providers\ConfigProvider;
use Igni\Application\Providers\ControllerProvider;
use Igni\Application\Providers\ServiceProvider;
use Igni\Container\DependencyResolver;
use Igni\Container\ServiceLocator;
use Psr\Container\ContainerInterface;
use Throwable;

/**
 * Main glue between all components.
 *
 * @package Igni\Application
 */
abstract class Application
{
    /**
     * @var ServiceLocator|ContainerInterface
     */
    private $container;

    /**
     * @var Config
     */
    private $config;

    /**
     * @var bool
     */
    private $initialized = false;

    /**
     * @var object[]|class[]
     */
    protected $modules;

    /**
     * @var DependencyResolver
     */
    protected $resolver;

    /**
     * Application constructor.
     *
     * @param ContainerInterface|null $container
     */
    public function __construct(ContainerInterface $container = null)
    {
        if ($container === null) {
            $container = new ServiceLocator();
        }

        $this->container = $container;
        $this->resolver = new DependencyResolver($this->container);

        if ($this->container->has(Config::class)) {
            $this->config = $this->container->get(Config::class);
        } else {
            $this->config = new Config([]);
        }

        $this->modules = [];
    }

    /**
     * Allows for application extension by modules.
     * Module can be any valid object or class name.
     *
     * @param $module
     */
    public function extend($module): void
    {
        if (is_object($module) || class_exists($module)) {
            $this->modules[] = $module;
        } else {
            throw ApplicationException::forInvalidModule($module);
        }
    }

    /**
     * Starts the application.
     * Initialize modules. Performs tasks to generate response for the client.
     *
     * @return mixed
     */
    abstract public function run();

    /**
     * Controller aggregator is used to register application's controllers.
     * @return ControllerAggregator
     */
    abstract public function getControllerAggregator(): ControllerAggregator;

    /**
     * Middleware aggregator is used to register application's middlewares.
     * @return MiddlewareAggregator
     */
    abstract public function getMiddlewareAggregator(): MiddlewareAggregator;

    /**
     * @return Config
     */
    public function getConfig(): Config
    {
        return $this->config;
    }

    public function getContainer(): ContainerInterface
    {
        return $this->container;
    }

    protected function handleOnBootListeners(): void
    {
        foreach ($this->modules as $module) {
            if ($module instanceof OnBootListener) {
                $module->onBoot($this);
            }
        }
    }

    protected function handleOnShutDownListeners(): void
    {
        foreach ($this->modules as $module) {
            if ($module instanceof OnShutDownListener) {
                $module->onShutDown($this);
            }
        }
    }

    protected function handleOnErrorListeners(Throwable $exception): Throwable
    {
        foreach ($this->modules as $module) {
            if ($module instanceof OnErrorListener) {
                $exception = $module->onError($this, $exception);
            }
        }

        return $exception;
    }

    protected function handleOnRunListeners(): void
    {
        foreach ($this->modules as $module) {
            if ($module instanceof OnRunListener) {
                $module->onRun($this);
            }
        }
    }

    protected function initialize(): void
    {
        if ($this->initialized) {
            return;
        }

        foreach ($this->modules as &$module) {
            $this->initializeModule($module);
        }

        $this->initialized = true;
    }

    protected function initializeModule(&$module): void
    {
        if (is_string($module)) {
            $module = $this->resolver->resolve($module);
        }

        if ($module instanceof ConfigProvider) {
            $module->provideConfig($this->getConfig());
        }

        if ($module instanceof ControllerProvider) {
            $module->provideControllers($this->getControllerAggregator());
        }

        if ($module instanceof ServiceProvider) {
            $module->provideServices($this->getContainer());
        }
    }
}


<?php declare(strict_types=1);

namespace Igni\Application\Http;

use Igni\Network\Http\Router;
use Igni\Network\Exception\RouterException;
use Igni\Network\Http\Route;
use Symfony\Component\Routing\Exception\MethodNotAllowedException as SymfonyMethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route as SymfonyRoute;
use Symfony\Component\Routing\RouteCollection;

/**
 * Wrapper for package symfony/router
 *
 * @package Igni\Http\Router
 */
class GenericRouter implements Router
{
    /** @var RouteCollection */
    protected $routeCollection;

    /** @var Route[] */
    protected $routes = [];

    public function __construct()
    {
        $this->routeCollection = new RouteCollection();
    }

    /**
     * Registers new route.
     *
     * @param Route $route
     */
    public function add(Route $route): void
    {
        if ($route instanceof Route) {
            $name = $route->getName();
        } else {
            $name = Route::generateNameFromPath($route->getPath());
        }

        $baseRoute = new SymfonyRoute($route->getPath());
        $baseRoute->setMethods($route->getMethods());

        $this->routeCollection->add($name, $baseRoute);
        $this->routes[$name] = $route;
    }

    /**
     * Finds route matching clients request.
     *
     * @param string $method request method.
     * @param string $path request path.
     * @return Route
     */
    public function find(string $method, string $path): Route
    {
        $matcher = new UrlMatcher($this->routeCollection, new RequestContext('/', $method));
        try {
            $route = $matcher->match($path);
        } catch (ResourceNotFoundException $exception) {
            throw RouterException::noRouteMatchesRequestedUri($path, $method);
        } catch (SymfonyMethodNotAllowedException $exception) {
            throw RouterException::methodNotAllowed($path, $exception->getAllowedMethods());
        }

        $routeName = $route['_route'];
        unset($route['_route']);

        return $this->routes[$routeName]->withAttributes($route);
    }
}


<?php declare(strict_types=1);

namespace Igni\Application;

use Igni\Application\Exception\ApplicationException;
use Igni\Application\Exception\ControllerException;
use Igni\Application\Http\Controller;
use Igni\Application\Http\MiddlewareAggregator;
use Igni\Application\Http\GenericRouter;
use Igni\Application\Providers\MiddlewareProvider;
use Igni\Network\Http\Middleware\CallableMiddleware;
use Igni\Network\Http\Middleware\ErrorMiddleware;
use Igni\Network\Http\Middleware\MiddlewarePipe;
use Igni\Network\Http\Response;
use Igni\Network\Http\Route;
use Igni\Network\Http\Router;
use Igni\Network\Http\ServerRequest;
use Igni\Network\Server\Client;
use Igni\Network\Server\HttpServer;
use Igni\Network\Server\OnRequestListener;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Zend\HttpHandlerRunner\Emitter\EmitterInterface;
use Zend\HttpHandlerRunner\Emitter\SapiEmitter;

/**
 * @package Igni\Application
 */
class HttpApplication extends Application implements
    ControllerAggregator,
    MiddlewareAggregator,
    MiddlewareInterface,
    RequestHandlerInterface,
    OnRequestListener
{
    /**
     * @var Router
     */
    private $router;

    /**
     * @var string[]|MiddlewareInterface[]
     */
    private $middleware = [];

    /**
     * @var MiddlewarePipe
     */
    private $pipeline;

    /**
     * @var EmitterInterface
     */
    private $emitter;

    /**
     * Application constructor.
     *
     * @param ContainerInterface|null $container
     */
    public function __construct(ContainerInterface $container = null)
    {
        parent::__construct($container);

        if ($this->getContainer()->has(Router::class)) {
            $this->router = $this->getContainer()->get(Router::class);
        } else {
            $this->router = new GenericRouter();
        }

        if ($this->getContainer()->has(EmitterInterface::class)) {
            $this->emitter = $this->getContainer()->get(EmitterInterface::class);
        } else {
            $this->emitter = new SapiEmitter();
        }
    }

    /**
     * While testing call this method before handle method.
     */
    public function startup(): void
    {
        $this->handleOnBootListeners();
        $this->initialize();
        $this->handleOnRunListeners();
    }

    /**
     * While testing, call this method after handle method.
     */
    public function shutdown(): void
    {
        $this->handleOnShutDownListeners();
    }

    /**
     * Startups and run application with/or without dedicated server.
     * Once application is run it will listen to incoming http requests,
     * and takes care of the entire request flow process.
     *
     * @param HttpServer|null $server
     */
    public function run(HttpServer $server = null): void
    {
        $this->startup();
        if ($server) {
            $server->addListener($this);
            $server->start();
        } else {
            $response = $this->handle(ServerRequest::fromGlobals());
            $this->emitter->emit($response);
            if ($response instanceof Response) {
                $response->end();
            }
        }

        $this->shutdown();
    }

    /**
     * Registers PSR-15 compatible middelware.
     * Middleware can be either callable object which accepts PSR-7 server request interface and returns
     * response interface, or just class name that implements psr-15 middleware or its instance.
     *
     * @param MiddlewareInterface|callable $middleware
     */
    public function use($middleware): void
    {
        if (!is_subclass_of($middleware, MiddlewareInterface::class)) {
            if (!is_callable($middleware)) {
                throw new ApplicationException(sprintf(
                    'Middleware must be either class or object that implements `%s`',
                    MiddlewareInterface::class
                ));
            }

            $middleware = new CallableMiddleware($middleware);
        }

        $this->middleware[] = $middleware;
    }

    public function register($controller, Route $route = null): void
    {
        if (is_callable($controller) && $route !== null) {
            $route = $route->withController($controller);
            $this->router->add($route);
            return;
        }

        if ($controller instanceof Controller) {
            /** @var Route $route */
            $route = $controller::getRoute();
            $route = $route->withController($controller);
            $this->router->add($route);
            return;
        }

        if (is_string($controller) && is_subclass_of($controller, Controller::class)) {
            /** @var Route $route */
            $route = $controller::getRoute();
            $route = $route->withController($controller);
            $this->router->add($route);
            return;
        }

        throw ApplicationException::forInvalidController($controller);
    }

    /**
     * Handles request flow process.
     *
     * @see MiddlewareInterface::process()
     *
     * @param ServerRequestInterface $request
     * @param RequestHandlerInterface $next
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        /** @var Route $route */
        $route = $this->router->find(
            $request->getMethod(),
            $request->getUri()->getPath()
        );

        $controller = $route->getController();

        if ($request instanceof ServerRequest) {
            $request = $request->withAttributes($route->getAttributes());
        }

        if (is_string($controller) &&
            class_exists($controller) &&
            is_subclass_of($controller, Controller::class)
        ) {
            /** @var Controller $instance */
            $instance = $this->resolver->resolve($controller);
            return $instance($request);
        }

        if (is_callable($controller)) {
            $response = $controller($request);
            if (!$response instanceof ResponseInterface) {
                throw ControllerException::forInvalidReturnValue();
            }

            return $response;
        }

        throw ControllerException::forMissingController($route->getPath());
    }

    /**
     * Runs application listeners and handles request flow process.
     *
     * @param ServerRequestInterface $request
     * @return ResponseInterface
     */
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $response = $this->getMiddlewarePipe()->handle($request);

        return $response;
    }

    /**
     * Decorator for handle method, used by server instance.
     * @see Application::handle()
     * @see Server::addListener()
     *
     * @param ResponseInterface $response
     * @param Client $client
     * @param ServerRequestInterface $request
     * @return ResponseInterface
     */
    public function onRequest(Client $client, ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        return $this->handle($request);
    }

    /**
     * Registers new controller that accepts get request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function get(string $route, callable $controller): void
    {
        $this->register($controller, Route::get($route));
    }

    /**
     * Registers new controller that accepts post request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function post(string $route, callable $controller): void
    {
        $this->register($controller, Route::post($route));
    }

    /**
     * Registers new controller that accepts put request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function put(string $route, callable $controller): void
    {
        $this->register($controller, Route::put($route));
    }

    /**
     * Registers new controller that accepts patch request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function patch(string $route, callable $controller): void
    {
        $this->register($controller, Route::patch($route));
    }

    /**
     * Registers new controller that accepts delete request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function delete(string $route, callable $controller): void
    {
        $this->register($controller, Route::delete($route));
    }

    /**
     * Registers new controller that accepts options request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function options(string $route, callable $controller): void
    {
        $this->register($controller, Route::options($route));
    }

    /**
     * Registers new controller that accepts head request
     * when request uri matches passed route pattern.
     *
     * @param string $route
     * @param callable $controller
     */
    public function head(string $route, callable $controller): void
    {
        $this->register($controller, Route::head($route));
    }

    /**
     * Registers new controller that listens on the passed route.
     *
     * @param Route $route
     * @param callable $controller
     */
    public function on(Route $route, callable $controller): void
    {
        $this->register($controller, $route);
    }

    /**
     * Returns application's controller aggregate.
     *
     * @return ControllerAggregator
     */
    public function getControllerAggregator(): ControllerAggregator
    {
        return $this;
    }

    /**
     * Middleware aggregator is used to register application's middlewares.
     *
     * @return MiddlewareAggregator
     */
    public function getMiddlewareAggregator(): MiddlewareAggregator
    {
        return $this;
    }

    protected function getMiddlewarePipe(): MiddlewarePipe
    {
        if ($this->pipeline) {
            return $this->pipeline;
        }

        return $this->pipeline = $this->composeMiddlewarePipe();
    }

    protected function initializeModule(&$module): void
    {
        parent::initializeModule($module);

        if ($module instanceof MiddlewareProvider) {
            $module->provideMiddleware($this->getMiddlewareAggregator());
        }
    }

    private function composeMiddlewarePipe(): MiddlewarePipe
    {
        $pipe = new MiddlewarePipe();
        $pipe->add(new ErrorMiddleware(function(Throwable $exception) {
            return $this->handleOnErrorListeners($exception);
        }));
        foreach ($this->middleware as $middleware) {
            if (is_string($middleware)) {
                $middleware = $this->resolver->resolve($middleware);
            }
            $pipe->add($middleware);
        }
        $pipe->add($this);

        return $pipe;
    }
}


<?php declare(strict_types=1);

namespace Igni\Application;

use Igni\Application\Exception\ConfigException;

/**
 * Application's config container.
 * Treats dots as an operator for accessing nested values.
 * If constant name is put in curly braces as a value, it wil be replaced
 * to the constant value.
 *
 * @example:
 * // Example usage.
 * $config = new Config();
 * $config->set('some.key', true);
 * $some = $config->get('some'); // returns ['key' => true]
 *
 * @package Igni\Application
 */
class Config
{
    /**
     * @var array
     */
    private $config;

    /**
     * Config constructor.
     *
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        $this->config = $config;
    }

    /**
     * Checks if config key exists.
     *
     * @param string $key
     * @return bool
     */
    public function has(string $key): bool
    {
        return $this->lookup($key) !== null;
    }

    private function lookup(string $key)
    {
        $result = $this->config;
        $key = explode('.', $key);
        foreach($key as $part) {
            if (!is_array($result) || !isset($result[$part])) {
                return null;
            }
            $result = $result[$part];
        }

        return $result;
    }

    /**
     * Gets value behind the key, or returns $default value if path does not exists.
     *
     * @param string $key
     * @param null $default
     * @return null|string|string[]
     */
    public function get(string $key, $default = null)
    {
        $result = $this->lookup($key);
        return $result === null ? $default : $this->fetchConstants($result);
    }

    /**
     * Merges one instance of Config class into current one and
     * returns current instance.
     *
     * @param Config $config
     * @return Config
     */
    public function merge(Config $config): Config
    {
        $this->config = array_merge_recursive($this->config, $config->config);

        return $this;
    }

    /**
     * Returns new instance of the config containing only values from the
     * given namespace.
     *
     * @param string $namespace
     * @return Config
     */
    public function extract(string $namespace): Config
    {
        $extracted = $this->get($namespace);
        if (!is_array($extracted)) {
            throw ConfigException::forExtractionFailure($namespace);
        }

        return new self($extracted);
    }

    /**
     * Sets new value.
     *
     * @param string $key
     * @param $value
     */
    public function set(string $key, $value): void
    {
        $key = explode('.', $key);
        $last = array_pop($key);
        $result = &$this->config;

        foreach ($key as $part) {
            if (!isset($result[$part]) || !is_array($result[$part])) {
                $result[$part] = [];
            }
            $result = &$result[$part];
        }
        $result[$last] = $value;
    }

    /**
     * Returns array representation of the config.
     *
     * @return array
     */
    public function toArray(): array
    {
        return $this->config;
    }

    /**
     * Returns flat array representation of the config, all nested values are stored
     * in keys containing path separated by dot.
     *
     * @return array
     */
    public function toFlatArray(): array
    {
        return self::flatten($this->config);
    }

    private static function flatten(array &$array, string $prefix = ''): array
    {
        $values = [];
        foreach ($array as $key => &$value) {
            if (is_array($value) && !empty($value)) {
                $values = array_merge($values, self::flatten($value, $prefix . $key . '.'));
            } else {
                $values[$prefix . $key] = $value;
            }
        }

        return $values;
    }

    private function fetchConstants($value)
    {
        if (!is_string($value)) {
            return $value;
        }
        return preg_replace_callback(
            '#\$\{([^{}]*)\}#',
            function($matches) {
                if (defined($matches[1])) {
                    return constant($matches[1]);
                }
                return $matches[0];
            },
            $value
        );
    }
}


<?php

namespace NotificationChannels\Discord;

use Illuminate\Support\ServiceProvider;
use NotificationChannels\Discord\Commands\SetupCommand;

class DiscordServiceProvider extends ServiceProvider
{
    /**
     * Register the application services.
     */
    public function register()
    {
        $this->app->bind('command.discord:setup', SetupCommand::class);
        $this->commands('command.discord:setup');
    }

    /**
     * Bootstrap the application services.
     */
    public function boot()
    {
        $token = $this->app->make('config')->get('services.discord.token');

        $this->app->when(Discord::class)
            ->needs('$token')
            ->give($token);

        $this->app->when(SetupCommand::class)
            ->needs('$token')
            ->give($token);
    }
}


<?php

namespace NotificationChannels\Discord\Commands;

use Exception;
use WebSocket\Client;
use Illuminate\Support\Arr;
use Illuminate\Console\Command;
use GuzzleHttp\Client as HttpClient;

class SetupCommand extends Command
{
    /**
     * @var string
     */
    protected $signature = 'discord:setup';

    /**
     * @var string
     */
    protected $description = "Add the bot to your server(s) and identify it with Discord's gateway.";

    /**
     * @var \GuzzleHttp\Client
     */
    protected $guzzle;

    /**
     * @var string
     */
    protected $token = null;

    /**
     * @var string
     */
    protected $gateway = 'wss://gateway.discord.gg';

    /**
     * @param \GuzzleHttp\Client $guzzle
     * @param string|null $token
     */
    public function __construct(HttpClient $guzzle, $token = null)
    {
        parent::__construct();

        $this->guzzle = $guzzle;
        $this->token = $token;
    }

    /**
     * Attempt to connect and identify a bot with the Discord websocket gateway.
     *
     * @return int|void
     */
    public function handle()
    {
        if (! $this->token) {
            $this->error('You must paste your Discord token (App Bot User token) into your `services.php` config file.');
            $this->error('View the README for more info: https://github.com/laravel-notification-channels/discord#installation');

            return -1;
        }

        if (! $this->confirm('Is the bot already added to your server?')) {
            $clientId = $this->ask('What is your Discord app client ID?');

            $this->warn('Add the bot to your server by visiting this link: https://discordapp.com/oauth2/authorize?&client_id='.$clientId.'&scope=bot&permissions=0');

            if (! $this->confirm('Continue?', true)) {
                return -1;
            }
        }

        $this->warn("Attempting to identify the bot with Discord's websocket gateway...");

        $this->gateway = $this->getGateway();

        $this->warn("Connecting to '$this->gateway'...");

        $client = $this->getSocket($this->gateway);

        // Discord requires all bots to connect via a websocket connection and
        // identify at least once before any API requests over HTTP are allowed.
        // https://discordapp.com/developers/docs/topics/gateway#gateway-identify
        $client->send(json_encode([
            'op' => 2,
            'd' => [
                'token' => $this->token,
                'v' => 3,
                'compress' => false,
                'properties' => [
                    '$os' => PHP_OS,
                    '$browser' => 'laravel-notification-channels-discord',
                    '$device' => 'laravel-notification-channels-discord',
                    '$referrer' => '',
                    '$referring_domain' => '',
                ],
            ],
        ]));

        $response = $client->receive();
        $identified = Arr::get(json_decode($response, true), 'op') === 10;

        if (! $identified) {
            $this->error("Discord responded with an error while trying to identify the bot: $response");

            return -1;
        }

        $this->info('Your bot has been identified by Discord and can now send API requests!');
    }

    /**
     * Get a websocket client for the given gateway.
     *
     * @param string $gateway
     *
     * @return \WebSocket\Client
     */
    public function getSocket($gateway)
    {
        return new Client($gateway);
    }

    /**
     * Get the URL of the gateway that the socket should connect to.
     *
     * @return string
     */
    public function getGateway()
    {
        $gateway = $this->gateway;

        try {
            $response = $this->guzzle->get('https://discordapp.com/api/gateway', [
                'headers' => [
                    'Authorization' => 'Bot '.$this->token,
                ],
            ]);

            $gateway = Arr::get(json_decode($response->getBody(), true), 'url', $gateway);
        } catch (Exception $e) {
            $this->warn("Could not get a websocket gateway address, defaulting to '{$gateway}'.");
        }

        return $gateway;
    }
}


<?php

namespace NotificationChannels\Discord\Exceptions;

use Exception;
use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;

class CouldNotSendNotification extends Exception
{
    /**
     * @param \Psr\Http\Message\ResponseInterface $response
     *
     * @return static
     */
    public static function serviceRespondedWithAnHttpError(ResponseInterface $response)
    {
        $message = "Discord responded with an HTTP error: {$response->getStatusCode()}";

        if ($error = Arr::get(json_decode($response->getBody(), true), 'message')) {
            $message .= ": $error";
        }

        return new static($message);
    }

    /**
     * @param array $response
     *
     * @return static
     */
    public static function serviceRespondedWithAnApiError(array $response)
    {
        return new static("Discord responded with an API error: {$response['code']}: {$response['message']}");
    }

    /**
     * @param \Exception $exception
     *
     * @return static
     */
    public static function serviceCommunicationError(Exception $exception)
    {
        return new static("Communication with Discord failed: {$exception->getCode()}: {$exception->getMessage()}");
    }
}


<?php

namespace NotificationChannels\Discord;

use Exception;
use Illuminate\Support\Arr;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\RequestException;
use NotificationChannels\Discord\Exceptions\CouldNotSendNotification;

class Discord
{
    /**
     * Discord API base URL.
     *
     * @var string
     */
    protected $baseUrl = 'https://discordapp.com/api';

    /**
     * API HTTP client.
     *
     * @var \GuzzleHttp\Client
     */
    protected $httpClient;

    /**
     * Discord API token.
     *
     * @var string
     */
    protected $token;

    /**
     * @param \GuzzleHttp\Client $http
     * @param string $token
     */
    public function __construct(HttpClient $http, $token)
    {
        $this->httpClient = $http;
        $this->token = $token;
    }

    /**
     * Send a message to a Discord channel.
     *
     * @param string $channel
     * @param array $data
     *
     * @return array
     */
    public function send($channel, array $data)
    {
        return $this->request('POST', 'channels/'.$channel.'/messages', $data);
    }

    /**
     * Create and/or get a private channel with a Discord user.
     *
     * @param mixed $user
     *
     * @return string
     */
    public function getPrivateChannel($user)
    {
        return $this->request('POST', 'users/@me/channels', ['recipient_id' => $user])['id'];
    }

    /**
     * Perform an HTTP request with the Discord API.
     *
     * @param string $verb
     * @param string $endpoint
     * @param array $data
     *
     * @return array
     *
     * @throws \NotificationChannels\Discord\Exceptions\CouldNotSendNotification
     */
    protected function request($verb, $endpoint, array $data)
    {
        $url = rtrim($this->baseUrl, '/').'/'.ltrim($endpoint, '/');

        try {
            $response = $this->httpClient->request($verb, $url, [
                'headers' => [
                    'Authorization' => 'Bot '.$this->token,
                ],
                'json' => $data,
            ]);
        } catch (RequestException $exception) {
            if ($response = $exception->getResponse()) {
                throw CouldNotSendNotification::serviceRespondedWithAnHttpError($response);
            }

            throw CouldNotSendNotification::serviceCommunicationError($exception);
        } catch (Exception $exception) {
            throw CouldNotSendNotification::serviceCommunicationError($exception);
        }

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

        if (Arr::get($body, 'code', 0) > 0) {
            throw CouldNotSendNotification::serviceRespondedWithAnApiError($body);
        }

        return $body;
    }
}


<?php

namespace NotificationChannels\Discord;

use Illuminate\Notifications\Notification;

class DiscordChannel
{
    /**
     * @var \NotificationChannels\Discord\Discord
     */
    protected $discord;

    /**
     * @param \NotificationChannels\Discord\Discord $discord
     */
    public function __construct(Discord $discord)
    {
        $this->discord = $discord;
    }

    /**
     * Send the given notification.
     *
     * @param mixed $notifiable
     * @param \Illuminate\Notifications\Notification $notification
     *
     * @return array
     *
     * @throws \NotificationChannels\Discord\Exceptions\CouldNotSendNotification
     */
    public function send($notifiable, Notification $notification)
    {
        if (! $channel = $notifiable->routeNotificationFor('discord')) {
            return;
        }

        $message = $notification->toDiscord($notifiable);

        return $this->discord->send($channel, [
            'content' => $message->body,
            'embed' => $message->embed,
        ]);
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Sender;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Model\Receiver;
use Apple\ApnPush\Protocol\ProtocolInterface;

/**
 * Default notification sender
 */
class Sender implements SenderInterface
{
    /**
     * @var ProtocolInterface
     */
    private $protocol;

    /**
     * Constructor.
     *
     * @param ProtocolInterface $protocol
     */
    public function __construct(ProtocolInterface $protocol)
    {
        $this->protocol = $protocol;
    }

    /**
     * {@inheritdoc}
     */
    public function send(Receiver $receiver, Notification $notification, bool $sandbox = false): void
    {
        $this->protocol->send($receiver, $notification, $sandbox);
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Model;

/**
 * Value object for alert object
 */
class Alert
{
    /**
     * @var string
     */
    private $title;

    /**
     * @var Localized
     */
    private $titleLocalized;

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

    /**
     * @var Localized
     */
    private $bodyLocalized;

    /**
     * @var Localized
     */
    private $actionLocalized;

    /**
     * @var string
     */
    private $launchImage = '';

    /**
     * Constructor.
     *
     * @param string $body
     * @param string $title
     */
    public function __construct(string $body = '', string $title = '')
    {
        $this->body = $body;
        $this->title = $title;
        $this->titleLocalized = new Localized('');
        $this->bodyLocalized = new Localized('');
        $this->actionLocalized = new Localized('');
    }

    /**
     * Set title
     *
     * @param string $title
     *
     * @return Alert
     */
    public function withTitle(string $title): Alert
    {
        $cloned = clone $this;

        $cloned->titleLocalized = new Localized('');
        $cloned->title = $title;

        return $cloned;
    }

    /**
     * With localized title
     *
     * @param Localized $localized
     *
     * @return Alert
     */
    public function withLocalizedTitle(Localized $localized): Alert
    {
        $cloned = clone $this;

        $cloned->title = '';
        $cloned->titleLocalized = $localized;

        return $cloned;
    }

    /**
     * Get title
     *
     * @return string
     */
    public function getTitle(): string
    {
        return $this->title;
    }

    /**
     * Get localized title
     *
     * @return Localized
     */
    public function getTitleLocalized(): Localized
    {
        return $this->titleLocalized;
    }

    /**
     * Set body
     *
     * @param string $body
     *
     * @return Alert
     */
    public function withBody(string $body): Alert
    {
        $cloned = clone $this;

        $cloned->bodyLocalized = new Localized('');
        $cloned->body = $body;

        return $cloned;
    }

    /**
     * Set localized body
     *
     * @param Localized $localized
     *
     * @return Alert
     */
    public function withBodyLocalized(Localized $localized): Alert
    {
        $cloned = clone $this;

        $cloned->body = '';
        $cloned->bodyLocalized = $localized;

        return $cloned;
    }

    /**
     * Get localized body
     *
     * @return Localized
     */
    public function getBodyLocalized(): Localized
    {
        return $this->bodyLocalized;
    }

    /**
     * Get body
     *
     * @return string
     */
    public function getBody(): string
    {
        return $this->body;
    }

    /**
     * With localized action
     *
     * @param Localized $localized
     *
     * @return Alert
     */
    public function withActionLocalized(Localized $localized): Alert
    {
        $cloned = clone $this;

        $cloned->actionLocalized = $localized;

        return $cloned;
    }

    /**
     * Get localized action
     *
     * @return Localized
     */
    public function getActionLocalized(): Localized
    {
        return $this->actionLocalized;
    }

    /**
     * Add launch image
     *
     * @param string $launchImage
     *
     * @return Alert
     */
    public function withLaunchImage(string $launchImage): Alert
    {
        $cloned = clone $this;

        $cloned->launchImage = $launchImage;

        return $cloned;
    }

    /**
     * Get launch image
     *
     * @return string
     */
    public function getLaunchImage(): string
    {
        return $this->launchImage;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Demo\Boot;

class Demo
{
    /**
     * Demo bootstrap
     */
    public static function boot()
    {
        self::checkVendors();
        include_once self::getVendorDirectory().'/autoload.php';
        self::includeConfigurationFile();
    }

    /**
     * Get vendor directory
     *
     * @return string
     */
    private static function getVendorDirectory(): string
    {
        return __DIR__.'/../../vendor';
    }

    /**
     * Check if vendors is installed
     */
    private static function checkVendors()
    {
        $autoloadFile = self::getVendorDirectory().'/autoload.php';

        if (!file_exists($autoloadFile)) {
            print sprintf(
                'The required packages not installed. Please download composer.phar and run install or update command.%s',
                PHP_EOL
            );

            exit(1);
        }
    }

    /**
     * Include configuration file with constants
     */
    private static function includeConfigurationFile()
    {
        static $included = false;

        if ($included) {
            // The configuration file already included
            return;
        }

        $file = __DIR__.'/config.php';

        if (!file_exists($file)) {
            print sprintf(
                'The configuration file "%s" was not found.%sPlease copy %s/config.php.dist to %s/config.php and modify new file for set own parameters.%s',
                $file,
                PHP_EOL,
                __DIR__,
                __DIR__,
                PHP_EOL
            );

            exit(1);
        }

        $included = true;
        include_once $file;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Visitor;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Visitor for add apn id header visitor
 */
class AddApnIdHeaderVisitor implements HttpProtocolVisitorInterface
{
    /**
     * {@inheritdoc}
     */
    public function visit(Notification $notification, Request $request): Request
    {
        $apnId = $notification->getApnId();

        if ($apnId) {
            $request = $request->withHeader('apns-id', $apnId->getValue());
        }

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Visitor;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Visitor for add expiration header to request
 */
class AddExpirationHeaderVisitor implements HttpProtocolVisitorInterface
{
    /**
     * {@inheritdoc}
     */
    public function visit(Notification $notification, Request $request): Request
    {
        $expiration = $notification->getExpiration();

        if ($expiration) {
            $request = $request->withHeader('apns-expiration', (string) $expiration->getValue());
        }

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Certificate;

/**
 * Certificate with content. You can use this certificate, if you save content to database
 * or another storage. Before get certificate file, this system create a temporary file certificate
 * and returns path to it. After close connection (destruct), the temporary file
 * will be removed.
 */
class ContentCertificate implements CertificateInterface
{
    /**
     * @var string
     */
    private $content;

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

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

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

    /**
     * Construct
     *
     * @param string $content
     * @param string $passPhrase
     * @param string $tmpDir
     */
    public function __construct(string $content, string $passPhrase, string $tmpDir)
    {
        $this->content = $content;
        $this->passPhrase = $passPhrase;
        $this->tmpDir = $tmpDir;
    }

    /**
     * {@inheritdoc}
     */
    public function getPath(): string
    {
        if ($this->certificateFilePath) {
            $this->removeTemporaryFile($this->certificateFilePath);
        }

        $this->certificateFilePath = $this->createTemporaryFile();
        file_put_contents($this->certificateFilePath, $this->content);

        return $this->certificateFilePath;
    }

    /**
     * {@inheritdoc}
     */
    public function getPassPhrase(): string
    {
        return $this->passPhrase;
    }

    /**
     * Implement __destruct
     * Remove temporary file
     */
    public function __destruct()
    {
        if ($this->certificateFilePath) {
            $this->removeTemporaryFile($this->certificateFilePath);
        }
    }

    /**
     * Create a temporary file
     *
     * @return string Path to temporary file
     *
     * @throws \RuntimeException
     */
    private function createTemporaryFile(): string
    {
        $tmpDir = $this->tmpDir;

        $tmpFileName = md5(uniqid((string) mt_rand(), true)).'.pem';

        $tmpFilePath = $tmpDir.'/'.$tmpFileName;

        $errorCode = $errorMessage = null;

        set_error_handler(function ($errCode, $errMessage) use (&$errorCode, &$errorMessage) {
            $errorCode = $errCode;
            $errorMessage = $errMessage;
        });

        if (!file_exists($tmpDir)) {
            mkdir($tmpDir, 0600, true);

            if ($errorCode || $errorMessage) {
                restore_error_handler();
                // Error create directory
                throw new \RuntimeException(sprintf(
                    'Can not create temporary directory "%s". Error: %s [%d].',
                    $tmpDir,
                    $errorMessage ?: 'Undefined',
                    $errorCode ?: '0'
                ));
            }
        }

        touch($tmpFilePath);

        if ($errorCode || $errorMessage) {
            restore_error_handler();

            // Error create file
            throw new \RuntimeException(sprintf(
                'Can not create temporary certificate file "%s". Error: %s [%d].',
                $tmpFilePath,
                $errorMessage ?: 'Undefined',
                $errorCode ?: '0'
            ));
        }

        restore_error_handler();

        return $tmpFilePath;
    }

    /**
     * Remove temporary file
     *
     * @param string $filePath
     */
    private function removeTemporaryFile($filePath): void
    {
        // Set custom error handler for suppress error
        set_error_handler(function () {
        });

        unlink($filePath);

        restore_error_handler();
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Model;

/**
 * Default APS data
 */
class Aps
{
    /**
     * @var Alert
     */
    private $alert;

    /**
     * @var int|null
     */
    private $badge = null;

    /**
     * @var string
     */
    private $sound = '';

    /**
     * @var bool
     */
    private $contentAvailable = false;

    /**
     * @var bool
    */
    private $mutableContent = false;

    /**
     * @var string
     */
    private $category = '';

    /**
     * @var string
     */
    private $threadId = '';

    /**
     * Constructor.
     *
     * @param Alert $alert
     */
    public function __construct(Alert $alert)
    {
        $this->alert = $alert;
    }

    /**
     * Set alert
     *
     * @param Alert $alert
     *
     * @return Aps
     */
    public function withAlert(Alert $alert): Aps
    {
        $cloned = clone $this;

        $cloned->alert = $alert;

        return $cloned;
    }

    /**
     * Get alert data
     *
     * @return Alert
     */
    public function getAlert(): Alert
    {
        return $this->alert;
    }

    /**
     * Set category
     *
     * @param string $category
     *
     * @return Aps
     */
    public function withCategory(string $category): Aps
    {
        $cloned = clone $this;

        $cloned->category = $category;

        return $cloned;
    }

    /**
     * Get category
     *
     * @return string
     */
    public function getCategory(): string
    {
        return $this->category;
    }

    /**
     * Set sound
     *
     * @param string $sound
     *
     * @return Aps
     */
    public function withSound(string $sound): Aps
    {
        $cloned = clone $this;

        $cloned->sound = $sound;

        return $cloned;
    }

    /**
     * Get sound
     *
     * @return string
     */
    public function getSound(): string
    {
        return $this->sound;
    }

    /**
     * Set badge
     *
     * @param int $badge
     *
     * @return Aps
     */
    public function withBadge(int $badge): Aps
    {
        $cloned = clone $this;

        $cloned->badge = (int) $badge;

        return $cloned;
    }

    /**
     * Get badge
     *
     * @return int|null
     */
    public function getBadge(): ?int
    {
        return $this->badge;
    }

    /**
     * Set content available option
     *
     * @param bool $contentAvailable
     *
     * @return Aps
     */
    public function withContentAvailable(bool $contentAvailable): Aps
    {
        $cloned = clone $this;

        $cloned->contentAvailable = $contentAvailable;

        return $cloned;
    }

    /**
     * Get content available option
     *
     * @return bool
     */
    public function isContentAvailable(): bool
    {
        return $this->contentAvailable;
    }

    /**
     * Set mutable content option
     *
     * @param bool $mutableContent
     *
     * @return Aps
     */
    public function withMutableContent(bool $mutableContent): Aps
    {
        $cloned = clone $this;

        $cloned->mutableContent = $mutableContent;

        return $cloned;
    }

    /**
     * Get mutable content option
     *
     * @return bool
     */
    public function isMutableContent(): bool
    {
        return $this->mutableContent;
    }

    /**
     * Set thread id
     *
     * @param string $threadId
     *
     * @return Aps
     */
    public function withThreadId(string $threadId): Aps
    {
        $cloned = clone $this;

        $cloned->threadId = $threadId;

        return $cloned;
    }

    /**
     * Get thread id
     *
     * @return string
     */
    public function getThreadId(): string
    {
        return $this->threadId;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Jwt\SignatureGenerator;

use Jose\Component\Core\JWK as WebTokenComponentJwk;
use Jose\Component\KeyManagement\JWKFactory as WebTokenComponentJWKFactory;
use Jose\Component\Signature\JWS as WebTokenComponentJws;
use Jose\Factory\JWKFactory;
use Jose\Factory\JWSFactory;

/**
 * The factory for try to resolve the JWT signature generator.
 */
class SignatureGeneratorFactory
{
    /**
     * @var array|callable[]
     */
    private static $resolvers = [];

    /**
     * Add resolver for try to resolve the signature generator
     *
     * @param callable $resolver
     */
    public static function addResolver(callable $resolver): void
    {
        array_unshift(self::$resolvers, $resolver);
    }

    /**
     * Try to resolve the signature generator
     *
     * @return SignatureGeneratorInterface
     *
     * @throws \LogicException
     */
    public static function resolve(): SignatureGeneratorInterface
    {
        self::addDefaultResolvers();

        foreach (self::$resolvers as $resolver) {
            if ($generator = $resolver()) {
                return $generator;
            }
        }

        throw new \LogicException('Cannot resolve available JWT Signature Generator.');
    }

    /**
     * Add default signature generator resolvers
     */
    private static function addDefaultResolvers(): void
    {
        static $added = false;

        if ($added) {
            return;
        }

        $added = true;

        self::addResolver([__CLASS__, 'tryResolveByWebTokenJwtSystem']);
        self::addResolver([__CLASS__, 'tryResolveBySpomkyLabsJoseSystem']);
    }

    /**
     * Try the resolve WebTokenJwtSignatureGenerator
     *
     * @return WebTokenJwtSignatureGenerator|null
     */
    private static function tryResolveByWebTokenJwtSystem(): ?WebTokenJwtSignatureGenerator
    {
        $requiredClasses = [
            WebTokenComponentJws::class,
            WebTokenComponentJwk::class,
            WebTokenComponentJWKFactory::class,
        ];

        foreach ($requiredClasses as $requiredClass) {
            if (!class_exists($requiredClass)) {
                return null;
            }
        }

        return new WebTokenJwtSignatureGenerator();
    }

    /**
     * Try to resolve SpomkyLabsJoseSignatureGenerator
     *
     * @return SpomkyLabsJoseSignatureGenerator|null
     */
    private static function tryResolveBySpomkyLabsJoseSystem(): ?SpomkyLabsJoseSignatureGenerator
    {
        $requiredClasses = [
            JWKFactory::class,
            JWSFactory::class,
        ];

        foreach ($requiredClasses as $requiredClass) {
            if (!class_exists($requiredClass)) {
                return null;
            }
        }

        return new SpomkyLabsJoseSignatureGenerator();
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Jwt;

/**
 * The JWT token with content for certificate. You can use this certificate, if you save content to database
 * or another storage. Before get certificate file, this system create a temporary file certificate
 * and returns path to it. After close connection (destruct), the temporary file
 * will be removed.
 */
class ContentJwt implements JwtInterface
{
    /**
     * @var string
     */
    private $teamId;

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

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

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

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

    /**
     * Constructor.
     *
     * @param string $teamId
     * @param string $key
     * @param string $content
     * @param string $tmpDir
     */
    public function __construct(string $teamId, string $key, string $content, string $tmpDir)
    {
        $this->teamId = $teamId;
        $this->key = $key;
        $this->content = $content;
        $this->tmpDir = $tmpDir;
    }

    /**
     * Implement __destruct
     * Remove temporary file
     */
    public function __destruct()
    {
        if ($this->certificateFilePath) {
            $this->removeTemporaryFile($this->certificateFilePath);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getTeamId(): string
    {
        return $this->teamId;
    }

    /**
     * {@inheritdoc}
     */
    public function getKey(): string
    {
        return $this->key;
    }

    /**
     * {@inheritdoc}
     */
    public function getPath(): string
    {
        if ($this->certificateFilePath) {
            $this->removeTemporaryFile($this->certificateFilePath);
        }

        $this->certificateFilePath = $this->createTemporaryFile();
        file_put_contents($this->certificateFilePath, $this->content);

        return $this->certificateFilePath;
    }

    /**
     * Create a temporary file
     *
     * @return string Path to temporary file
     *
     * @throws \RuntimeException
     */
    private function createTemporaryFile(): string
    {
        $tmpDir = $this->tmpDir;

        $tmpFileName = md5(uniqid((string) mt_rand(), true)).'.p8';

        $tmpFilePath = $tmpDir.'/'.$tmpFileName;

        $errorCode = $errorMessage = null;

        set_error_handler(function ($errCode, $errMessage) use (&$errorCode, &$errorMessage) {
            $errorCode = $errCode;
            $errorMessage = $errMessage;
        });

        if (!file_exists($tmpDir)) {
            mkdir($tmpDir, 0600, true);

            if ($errorCode || $errorMessage) {
                restore_error_handler();
                // Error create directory
                throw new \RuntimeException(sprintf(
                    'Can not create temporary directory "%s". Error: %s [%d].',
                    $tmpDir,
                    $errorMessage ?: 'Undefined',
                    $errorCode ?: '0'
                ));
            }
        }

        touch($tmpFilePath);

        if ($errorCode || $errorMessage) {
            restore_error_handler();

            // Error create file
            throw new \RuntimeException(sprintf(
                'Can not create temporary certificate file "%s". Error: %s [%d].',
                $tmpFilePath,
                $errorMessage ?: 'Undefined',
                $errorCode ?: '0'
            ));
        }

        restore_error_handler();

        return $tmpFilePath;
    }

    /**
     * Remove temporary file
     *
     * @param string $filePath
     */
    private function removeTemporaryFile($filePath): void
    {
        // Set custom error handler for suppress error
        set_error_handler(function () {
        });

        unlink($filePath);

        restore_error_handler();
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Model;

/**
 * Payload model
 */
class Payload
{
    /**
     * @var Aps
     */
    private $aps;

    /**
     * @var array
     */
    private $customData;

    /**
     * Constructor.
     *
     * @param Aps   $apsData
     * @param array $customData
     */
    public function __construct(Aps $apsData, array $customData = [])
    {
        $this->aps = $apsData;
        $this->customData = $customData;
    }

    /**
     * Create new payload with body only.
     *
     * @param string $body
     *
     * @return Payload
     */
    public static function createWithBody(string $body): Payload
    {
        return new self(new Aps(new Alert($body)));
    }

    /**
     * Set aps
     *
     * @param Aps $aps
     *
     * @return Payload
     */
    public function withAps(Aps $aps): Payload
    {
        $cloned = clone $this;

        $cloned->aps = $aps;

        return $cloned;
    }

    /**
     * Get APS data
     *
     * @return Aps
     */
    public function getAps(): Aps
    {
        return $this->aps;
    }

    /**
     * Add or replace custom data
     *
     * @param string $name
     * @param mixed  $value
     *
     * @return Payload
     *
     * @throws \InvalidArgumentException
     */
    public function withCustomData(string $name, $value): Payload
    {
        if ($value && !is_array($value) && !is_scalar($value) && !$value instanceof \JsonSerializable) {
            throw new \InvalidArgumentException(sprintf(
                'The custom data value should be a scalar or \JsonSerializable instance, but "%s" given.',
                is_object($value) ? get_class($value) : gettype($value)
            ));
        }

        $cloned = clone $this;

        $cloned->customData[$name] = $value;

        return $cloned;
    }

    /**
     * Get custom data
     *
     * @return array
     */
    public function getCustomData(): array
    {
        return $this->customData;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http;

/**
 * Object for presentation http request
 */
class Request
{
    /**
     * @var string
     */
    private $url;

    /**
     * @var array
     */
    private $headers;

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

    /**
     * @var string
     */
    private $certificate = '';

    /**
     * @var string
     */
    private $certificatePassPhrase = '';

    /**
     * Constructor.
     *
     * @param string $url
     * @param string $content
     * @param array  $headers
     */
    public function __construct(string $url, string $content, array $headers = [])
    {
        $this->url = $url;
        $this->headers = $headers;
        $this->content = $content;
    }

    /**
     * Get url
     *
     * @return string
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * Add or replace header
     *
     * @param string $name
     * @param string $value
     *
     * @return Request
     */
    public function withHeader(string $name, string $value): Request
    {
        $cloned = clone $this;

        $cloned->headers[$name] = $value;

        return $cloned;
    }

    /**
     * Add multiple headers
     *
     * @param array $headers
     *
     * @return Request
     */
    public function withHeaders(array $headers): Request
    {
        $cloned = clone $this;

        foreach ($headers as $name => $value) {
            $cloned = $cloned->withHeader($name, $value);
        }

        return $cloned;
    }

    /**
     * Get headers
     *
     * @return array
     */
    public function getHeaders(): array
    {
        return $this->headers;
    }

    /**
     * Get content
     *
     * @return string
     */
    public function getContent(): string
    {
        return $this->content;
    }

    /**
     * Use certificate for send request
     *
     * @param string $certificate
     *
     * @return Request
     */
    public function withCertificate(string $certificate): Request
    {
        $cloned = clone $this;

        $cloned->certificate = $certificate;

        return $cloned;
    }

    /**
     * Get certificate
     *
     * @return string
     */
    public function getCertificate(): string
    {
        return $this->certificate;
    }

    /**
     * Use pass phrase of certificate for send request
     *
     * @param string $passPhrase
     *
     * @return Request
     */
    public function withCertificatePassPhrase(string $passPhrase): Request
    {
        $cloned = clone $this;

        $cloned->certificatePassPhrase = $passPhrase;

        return $cloned;
    }

    /**
     * Get certificate pass phrase
     *
     * @return string
     */
    public function getCertificatePassPhrase(): string
    {
        return $this->certificatePassPhrase;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Visitor;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Chain visitor for visit for notification and request before send request
 */
class HttpProtocolChainVisitor implements HttpProtocolVisitorInterface
{
    /**
     * @var \SplPriorityQueue|HttpProtocolVisitorInterface[]
     */
    private $visitors;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->visitors = new \SplPriorityQueue();
    }

    /**
     * Add visitor to chain
     *
     * @param HttpProtocolVisitorInterface $visitor
     * @param int                          $priority
     */
    public function add(HttpProtocolVisitorInterface $visitor, int $priority = 0): void
    {
        $this->visitors->insert($visitor, $priority);
    }

    /**
     * {@inheritdoc}
     */
    public function visit(Notification $notification, Request $request): Request
    {
        // Clone all visitors because \SplPriorityQueue remove object after iteration
        $visitors = clone $this->visitors;

        foreach ($visitors as $visitor) {
            $request = $visitor->visit($notification, $request);
        }

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Authenticator;

use Apple\ApnPush\Certificate\CertificateInterface;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Authenticate request via certificate
 */
class CertificateAuthenticator implements AuthenticatorInterface
{
    /**
     * @var CertificateInterface
     */
    private $certificate;

    /**
     * Constructor.
     *
     * @param CertificateInterface $certificate
     */
    public function __construct(CertificateInterface $certificate)
    {
        $this->certificate = $certificate;
    }

    /**
     * {@inheritdoc}
     */
    public function authenticate(Request $request): Request
    {
        $request = $request->withCertificate($this->certificate->getPath());
        $request = $request->withCertificatePassPhrase($this->certificate->getPassPhrase());

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol;

use Apple\ApnPush\Encoder\PayloadEncoderInterface;
use Apple\ApnPush\Exception\SendNotification\SendNotificationException;
use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Model\Receiver;
use Apple\ApnPush\Protocol\Http\Authenticator\AuthenticatorInterface;
use Apple\ApnPush\Protocol\Http\ExceptionFactory\ExceptionFactoryInterface;
use Apple\ApnPush\Protocol\Http\Request;
use Apple\ApnPush\Protocol\Http\Sender\Exception\HttpSenderException;
use Apple\ApnPush\Protocol\Http\Sender\HttpSenderInterface;
use Apple\ApnPush\Protocol\Http\UriFactory\UriFactoryInterface;
use Apple\ApnPush\Protocol\Http\Visitor\HttpProtocolVisitorInterface;

/**
 * Implement HTTP protocol for send push notification
 */
class HttpProtocol implements ProtocolInterface
{
    /**
     * @var AuthenticatorInterface
     */
    private $authenticator;

    /**
     * @var HttpSenderInterface
     */
    private $httpSender;

    /**
     * @var PayloadEncoderInterface
     */
    private $payloadEncoder;

    /**
     * @var UriFactoryInterface
     */
    private $uriFactory;

    /**
     * @var HttpProtocolVisitorInterface
     */
    private $visitor;

    /**
     * @var ExceptionFactoryInterface
     */
    private $exceptionFactory;

    /**
     * Constructor.
     *
     * @param AuthenticatorInterface       $authenticator
     * @param HttpSenderInterface          $httpSender
     * @param PayloadEncoderInterface      $payloadEncoder
     * @param UriFactoryInterface          $uriFactory
     * @param HttpProtocolVisitorInterface $visitor
     * @param ExceptionFactoryInterface    $exceptionFactory
     */
    public function __construct(AuthenticatorInterface $authenticator, HttpSenderInterface $httpSender, PayloadEncoderInterface $payloadEncoder, UriFactoryInterface $uriFactory, HttpProtocolVisitorInterface $visitor, ExceptionFactoryInterface $exceptionFactory)
    {
        $this->authenticator = $authenticator;
        $this->httpSender = $httpSender;
        $this->payloadEncoder = $payloadEncoder;
        $this->uriFactory = $uriFactory;
        $this->visitor = $visitor;
        $this->exceptionFactory = $exceptionFactory;
    }

    /**
     * {@inheritdoc}
     *
     * @throws HttpSenderException
     */
    public function send(Receiver $receiver, Notification $notification, bool $sandbox): void
    {
        try {
            $this->doSend($receiver, $notification, $sandbox);
        } catch (HttpSenderException $e) {
            $this->httpSender->close();

            throw $e;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function closeConnection(): void
    {
        $this->httpSender->close();
    }

    /**
     * Inner send process
     *
     * @param Receiver     $receiver
     * @param Notification $notification
     * @param bool         $sandbox
     *
     * @throws SendNotificationException
     * @throws HttpSenderException
     */
    private function doSend(Receiver $receiver, Notification $notification, bool $sandbox): void
    {
        $payloadEncoded = $this->payloadEncoder->encode($notification->getPayload());
        $uri = $this->uriFactory->create($receiver->getToken(), $sandbox);

        $request = new Request($uri, $payloadEncoded);

        $headers = [
            'content-type' => 'application/json',
            'accept'       => 'application/json',
            'apns-topic'   => $receiver->getTopic(),
        ];

        $request = $request->withHeaders($headers);
        $request = $this->authenticator->authenticate($request);

        $request = $this->visitor->visit($notification, $request);

        $response = $this->httpSender->send($request);

        if ($response->getStatusCode() !== 200) {
            throw $this->exceptionFactory->create($response);
        }
    }
}