<?php

namespace Flugg\Responder\Exceptions;

use Exception;
use Flugg\Responder\Contracts\Responder;
use Flugg\Responder\Exceptions\Http\HttpException;
use Flugg\Responder\Exceptions\Http\PageNotFoundException;
use Flugg\Responder\Exceptions\Http\RelationNotFoundException;
use Flugg\Responder\Exceptions\Http\UnauthenticatedException;
use Flugg\Responder\Exceptions\Http\UnauthorizedException;
use Flugg\Responder\Exceptions\Http\ValidationFailedException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException as BaseRelationNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * A trait used by exception handlers to transform and render error responses.
 *
 * @package flugger/laravel-responder
 * @author  Alexander Tømmerås <flugged@gmail.com>
 * @license The MIT License
 */
trait ConvertsExceptions
{
    /**
     * A list of default exception types that should not be converted.
     *
     * @var array
     */
    protected $dontConvert = [];

    /**
     * Convert an exception to another exception
     *
     * @param  \Exception $exception
     * @param  array      $convert
     * @return void
     */
    protected function convert(Exception $exception, array $convert)
    {
        foreach ($convert as $source => $target) {
            if ($exception instanceof $source) {
                if (is_callable($target)) {
                    $target($exception);
                }

                throw new $target;
            }
        }
    }

    /**
     * Convert a default exception to an API exception.
     *
     * @param  \Exception $exception
     * @return void
     */
    protected function convertDefaultException(Exception $exception)
    {
        $this->convert($exception, array_diff_key([
            AuthenticationException::class => UnauthenticatedException::class,
            AuthorizationException::class => UnauthorizedException::class,
            NotFoundHttpException::class => PageNotFoundException::class,
            ModelNotFoundException::class => PageNotFoundException::class,
            BaseRelationNotFoundException::class => RelationNotFoundException::class,
            ValidationException::class => function ($exception) {
                throw new ValidationFailedException($exception->validator);
            },
        ], array_flip($this->dontConvert)));
    }

    /**
     * Render an error response from an API exception.
     *
     * @param  \Flugg\Responder\Exceptions\Http\HttpException $exception
     * @return \Illuminate\Http\JsonResponse
     */
    protected function renderResponse(HttpException $exception): JsonResponse
    {
        return app(Responder::class)
            ->error($exception->errorCode(), $exception->message())
            ->data($exception->data())
            ->respond($exception->statusCode(), $exception->getHeaders());
    }
}


<?php

namespace Flugg\Responder\Http;

use Flugg\Responder\Http\Responses\ErrorResponseBuilder;
use Flugg\Responder\Http\Responses\SuccessResponseBuilder;
use Flugg\Responder\Responder;

/**
 * A trait to be used by controllers to easily make success- and error responses.
 *
 * @package flugger/laravel-responder
 * @author  Alexander Tømmerås <flugged@gmail.com>
 * @license The MIT License
 */
trait MakesResponses
{
    /**
     * Build a successful response.
     *
     * @param  mixed                                                          $data
     * @param  callable|string|\Flugg\Responder\Transformers\Transformer|null $transformer
     * @param  string|null                                                    $resourceKey
     * @return \Flugg\Responder\Http\Responses\SuccessResponseBuilder
     */
    public function success($data = null, $transformer = null, string $resourceKey = null): SuccessResponseBuilder
    {
        return app(Responder::class)->success(...func_get_args());
    }

    /**
     * Build an error response.
     *
     * @param  mixed|null  $errorCode
     * @param  string|null $message
     * @return \Flugg\Responder\Http\Responses\ErrorResponseBuilder
     */
    public function error($errorCode = null, string $message = null): ErrorResponseBuilder
    {
        return app(Responder::class)->error(...func_get_args());
    }
}


<?php

namespace Flugg\Responder\Http\Responses\Decorators;

use Illuminate\Http\JsonResponse;

/**
 * A decorator class for adding status code information to the response data.
 *
 * @package flugger/laravel-responder
 * @author  Alexander Tømmerås <flugged@gmail.com>
 * @license The MIT License
 */
class StatusCodeDecorator extends ResponseDecorator
{
    /**
     * Generate a JSON response.
     *
     * @param  array $data
     * @param  int   $status
     * @param  array $headers
     * @return \Illuminate\Http\JsonResponse
     */
    public function make(array $data, int $status, array $headers = []): JsonResponse
    {
        return $this->factory->make(array_merge([
            'status' => $status,
        ], $data), $status, $headers);
    }
}


<?php

namespace Flugg\Responder\Http\Responses\Decorators;

use Illuminate\Http\JsonResponse;

/**
 * A decorator class for generating JSON using the JSON_PRETTY_PRINT option.
 *
 * @package flugger/laravel-responder
 * @author  Alexander Tømmerås <flugged@gmail.com>
 * @license The MIT License
 */
class PrettyPrintDecorator extends ResponseDecorator
{
    /**
     * Generate a JSON response.
     *
     * @param  array $data
     * @param  int   $status
     * @param  array $headers
     * @return \Illuminate\Http\JsonResponse
     */
    public function make(array $data, int $status, array $headers = []): JsonResponse
    {
        $response = $this->factory->make($data, $status, $headers);

        $response->setEncodingOptions($response->getEncodingOptions() | JSON_PRETTY_PRINT);

        return $response;
    }
}


<?php

namespace Flugg\Responder;

use Flugg\Responder\Serializers\NoopSerializer;

/**
 * A class responsible for obtaining a transformation to transform data without serializing.
 *
 * @package flugger/laravel-responder
 * @author  Alexander Tømmerås <flugged@gmail.com>
 * @license The MIT License
 */
class Transformation
{
    /**
     * A builder used to build transformed arrays.
     *
     * @var \Flugg\Responder\TransformBuilder
     */
    protected $transformBuilder;

    /**
     * Construct the service class.
     *
     * @param \Flugg\Responder\TransformBuilder $transformBuilder
     */
    public function __construct(TransformBuilder $transformBuilder)
    {
        $this->transformBuilder = $transformBuilder;
    }

    /**
     * Make a new transformation to transform data without serializing.
     *
     * @param  mixed                                                          $data
     * @param  \Flugg\Responder\Transformers\Transformer|callable|string|null $transformer
     * @param  string|null                                                    $resourceKey
     * @return \Flugg\Responder\TransformBuilder
     */
    public function make($data = null, $transformer = null, string $resourceKey = null): TransformBuilder
    {
        return $this->transformBuilder->resource($data, $transformer, $resourceKey)->serializer(new NoopSerializer);
    }
}


<?php

namespace Flugg\Responder\Http\Responses\Decorators;

use Illuminate\Http\JsonResponse;

/**
 * A decorator class for escaping HTML entities in strings on the response.
 *
 * @package flugger/laravel-responder
 * @author  Paolo Caleffi <p.caleffi@dreamonkey.com>
 * @license The MIT License
 */
class EscapeHtmlDecorator extends ResponseDecorator
{
    /**
     * Generate a JSON response.
     *
     * @param  array $data
     * @param  int   $status
     * @param  array $headers
     * @return \Illuminate\Http\JsonResponse
     */
    public function make(array $data, int $status, array $headers = []): JsonResponse
    {
        array_walk_recursive($data, function (&$value) {
            if (is_string($value)) {
                $value = e($value);
            }
        });

        return $this->factory->make($data, $status, $headers);
    }
}


<?php

namespace PhpSchool\CliMenu;

use PhpSchool\CliMenu\Exception\InvalidTerminalException;
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
use PhpSchool\CliMenu\Input\InputIO;
use PhpSchool\CliMenu\Input\Number;
use PhpSchool\CliMenu\Input\Password;
use PhpSchool\CliMenu\Input\Text;
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
use PhpSchool\CliMenu\MenuItem\SplitItem;
use PhpSchool\CliMenu\MenuItem\StaticItem;
use PhpSchool\CliMenu\Dialogue\Confirm;
use PhpSchool\CliMenu\Dialogue\Flash;
use PhpSchool\CliMenu\Terminal\TerminalFactory;
use PhpSchool\CliMenu\Util\StringUtil as s;
use PhpSchool\Terminal\InputCharacter;
use PhpSchool\Terminal\NonCanonicalReader;
use PhpSchool\Terminal\Terminal;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class CliMenu
{
    /**
     * @var Terminal
     */
    protected $terminal;

    /**
     * @var MenuStyle
     */
    protected $style;

    /**
     * @var ?string
     */
    protected $title;

    /**
     * @var MenuItemInterface[]
     */
    protected $items = [];

    /**
     * @var int|null
     */
    protected $selectedItem;

    /**
     * @var bool
     */
    protected $open = false;

    /**
     * @var CliMenu|null
     */
    protected $parent;

    /**
     * @var array
     */
    protected $defaultControlMappings = [
        '^P' => InputCharacter::UP,
        'k'  => InputCharacter::UP,
        '^K' => InputCharacter::DOWN,
        'j'  => InputCharacter::DOWN,
        "\r" => InputCharacter::ENTER,
        ' '  => InputCharacter::ENTER,
        'l'  => InputCharacter::LEFT,
        'm'  => InputCharacter::RIGHT,
    ];

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

    /**
     * @var Frame
     */
    private $currentFrame;

    public function __construct(
        ?string $title,
        array $items,
        Terminal $terminal = null,
        MenuStyle $style = null
    ) {
        $this->title      = $title;
        $this->items      = $items;
        $this->terminal   = $terminal ?: TerminalFactory::fromSystem();
        $this->style      = $style ?: new MenuStyle($this->terminal);

        $this->selectFirstItem();
    }

    /**
     * Configure the terminal to work with CliMenu
     */
    protected function configureTerminal() : void
    {
        $this->assertTerminalIsValidTTY();

        $this->terminal->disableCanonicalMode();
        $this->terminal->disableEchoBack();
        $this->terminal->disableCursor();
        $this->terminal->clear();
    }

    /**
     * Revert changes made to the terminal
     */
    protected function tearDownTerminal() : void
    {
        $this->terminal->restoreOriginalConfiguration();
        $this->terminal->enableCursor();
    }

    private function assertTerminalIsValidTTY() : void
    {
        if (!$this->terminal->isInteractive()) {
            throw new InvalidTerminalException('Terminal is not interactive (TTY)');
        }
    }

    public function setTitle(string $title) : void
    {
        $this->title = $title;
    }

    public function setParent(CliMenu $parent) : void
    {
        $this->parent = $parent;
    }

    public function getParent() : ?CliMenu
    {
        return $this->parent;
    }

    public function getTerminal() : Terminal
    {
        return $this->terminal;
    }

    public function isOpen() : bool
    {
        return $this->open;
    }

    /**
     * Add a new Item to the menu
     */
    public function addItem(MenuItemInterface $item) : void
    {
        $this->items[] = $item;

        $this->selectFirstItem();
    }

    /**
     * Add multiple Items to the menu
     */
    public function addItems(array $items) : void
    {
        foreach ($items as $item) {
            $this->items[] = $item;
        }

        $this->selectFirstItem();
    }

    /**
     * Set Items of the menu
     */
    public function setItems(array $items) : void
    {
        $this->selectedItem = null;
        $this->items = $items;

        $this->selectFirstItem();
    }

    /**
     * Set the selected pointer to the first selectable item
     */
    private function selectFirstItem() : void
    {
        if (null === $this->selectedItem) {
            foreach ($this->items as $key => $item) {
                if ($item->canSelect()) {
                    $this->selectedItem = $key;
                    break;
                }
            }
        }
    }

    /**
     * Disables the built-in VIM control mappings
     */
    public function disableDefaultControlMappings() : void
    {
        $this->defaultControlMappings = [];
    }

    /**
     * Set default control mappings
     */
    public function setDefaultControlMappings(array $defaultControlMappings) : void
    {
        $this->defaultControlMappings = $defaultControlMappings;
    }

    /**
     * Adds a custom control mapping
     */
    public function addCustomControlMapping(string $input, callable $callable) : void
    {
        if (isset($this->defaultControlMappings[$input]) || isset($this->customControlMappings[$input])) {
            throw new \InvalidArgumentException('Cannot rebind this input');
        }

        $this->customControlMappings[$input] = $callable;
    }

    /**
     * Shorthand function to add multiple custom control mapping at once
     */
    public function addCustomControlMappings(array $map) : void
    {
        foreach ($map as $input => $callable) {
            $this->addCustomControlMapping($input, $callable);
        }
    }

    /**
     * Removes a custom control mapping
     */
    public function removeCustomControlMapping(string $input) : void
    {
        if (!isset($this->customControlMappings[$input])) {
            throw new \InvalidArgumentException('This input is not registered');
        }

        unset($this->customControlMappings[$input]);
    }

    /**
     * Display menu and capture input
     */
    private function display() : void
    {
        $this->draw();

        $reader = new NonCanonicalReader($this->terminal);
        $reader->addControlMappings($this->defaultControlMappings);

        while ($this->isOpen()) {
            $char = $reader->readCharacter();
            if (!$char->isHandledControl()) {
                $rawChar = $char->get();
                if (isset($this->customControlMappings[$rawChar])) {
                    $this->customControlMappings[$rawChar]($this);
                }
                continue;
            }

            switch ($char->getControl()) {
                case InputCharacter::UP:
                case InputCharacter::DOWN:
                    $this->moveSelectionVertically($char->getControl());
                    $this->draw();
                    break;
                case InputCharacter::LEFT:
                case InputCharacter::RIGHT:
                    $this->moveSelectionHorizontally($char->getControl());
                    $this->draw();
                    break;
                case InputCharacter::ENTER:
                    $this->executeCurrentItem();
                    break;
            }
        }
    }

    /**
     * Move the selection in a given direction, up / down
     */
    protected function moveSelectionVertically(string $direction) : void
    {
        $itemKeys = array_keys($this->items);

        $increments = 0;

        do {
            $increments++;

            if ($increments > count($itemKeys)) {
                //full cycle detected, there must be no selected items
                //in the menu, so stop trying to select one.
                return;
            }

            $direction === 'UP'
                ? $this->selectedItem--
                : $this->selectedItem++;

            if ($this->selectedItem !== null && !array_key_exists($this->selectedItem, $this->items)) {
                $this->selectedItem  = $direction === 'UP'
                    ? end($itemKeys)
                    : reset($itemKeys);
            }
        } while (!$this->canSelect());
    }

    /**
     * Move the selection in a given direction, left / right
     */
    protected function moveSelectionHorizontally(string $direction) : void
    {
        if (!$this->items[$this->selectedItem] instanceof SplitItem) {
            return;
        }

        /** @var SplitItem $item */
        $item = $this->items[$this->selectedItem];
        $itemKeys = array_keys($item->getItems());
        $selectedItemIndex = $item->getSelectedItemIndex();

        do {
            $direction === 'LEFT'
                ? $selectedItemIndex--
                : $selectedItemIndex++;

            if (!array_key_exists($selectedItemIndex, $item->getItems())) {
                $selectedItemIndex = $direction === 'LEFT'
                    ? end($itemKeys)
                    : reset($itemKeys);
            }
        } while (!$item->canSelectIndex($selectedItemIndex));

        $item->setSelectedItemIndex($selectedItemIndex);
    }

    /**
     * Can the currently selected item actually be selected?
     *
     * For example:
     *  selectable item -> yes
     *  static item -> no
     *  split item with only static items -> no
     *  split item with at least one selectable item -> yes
     *
     * @return bool
     */
    private function canSelect() : bool
    {
        return $this->items[$this->selectedItem]->canSelect();
    }

    /**
     * Retrieve the item the user actually selected
     *
     */
    public function getSelectedItem() : MenuItemInterface
    {
        if (null === $this->selectedItem) {
            throw new \RuntimeException('No selected item');
        }

        $item = $this->items[$this->selectedItem];
        return $item instanceof SplitItem
            ? $item->getSelectedItem()
            : $item;
    }

    public function setSelectedItem(MenuItemInterface $item) : void
    {
        $key = array_search($item, $this->items, true);

        if (false === $key) {
            throw new \InvalidArgumentException('Item does not exist in menu');
        }

        $this->selectedItem = $key;
    }

    public function executeAsSelected(MenuItemInterface $item) : void
    {
        $current = $this->items[$this->selectedItem];
        $this->setSelectedItem($item);
        $this->executeCurrentItem();
        $this->setSelectedItem($current);
    }

    /**
     * Execute the current item
     */
    protected function executeCurrentItem() : void
    {
        $item = $this->getSelectedItem();

        if ($item->canSelect()) {
            $callable = $item->getSelectAction();
            if ($callable) {
                $callable($this);
            }
        }
    }

    /**
     * If true we clear the whole terminal screen, useful
     * for example when reducing the width of the menu, to not
     * leave leftovers of the previous wider menu.
     *
     * Redraw the menu
     */
    public function redraw(bool $clear = false) : void
    {
        if ($clear) {
            $this->terminal->clear();
        }

        $this->assertOpen();
        $this->draw();
    }

    private function assertOpen() : void
    {
        if (!$this->isOpen()) {
            throw new MenuNotOpenException;
        }
    }

    /**
     * Draw the menu to STDOUT
     */
    protected function draw() : void
    {
        $frame = new Frame;

        $frame->newLine(2);

        if ($this->style->getBorderTopWidth() > 0) {
            $frame->addRows($this->style->getBorderTopRows());
        }

        if ($this->style->getPaddingTopBottom() > 0) {
            $frame->addRows($this->style->getPaddingTopBottomRows());
        }

        if ($this->title) {
            $frame->addRows($this->drawMenuItem(new StaticItem($this->title)));
            $frame->addRows($this->drawMenuItem(new LineBreakItem($this->style->getTitleSeparator())));
        }

        array_map(function ($item, $index) use ($frame) {
            $frame->addRows($this->drawMenuItem($item, $index === $this->selectedItem));
        }, $this->items, array_keys($this->items));

        if ($this->style->getPaddingTopBottom() > 0) {
            $frame->addRows($this->style->getPaddingTopBottomRows());
        }

        if ($this->style->getBorderBottomWidth() > 0) {
            $frame->addRows($this->style->getBorderBottomRows());
        }

        $frame->newLine(2);

        $this->terminal->moveCursorToTop();
        foreach ($frame->getRows() as $row) {
            if ($row == "\n") {
                $this->terminal->clearLine();
            }
            $this->terminal->write($row);
        }
        $this->terminal->clearDown();

        $this->currentFrame = $frame;
    }

    /**
     * Draw a menu item
     */
    protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) : array
    {
        $rows = $item->getRows($this->style, $selected);

        if ($item instanceof SplitItem) {
            $selected = false;
        }

        $invertedColoursSetCode = $selected
            ? $this->style->getInvertedColoursSetCode()
            : '';
        $invertedColoursUnsetCode = $selected
            ? $this->style->getInvertedColoursUnsetCode()
            : '';

        if ($this->style->getBorderLeftWidth() || $this->style->getBorderRightWidth()) {
            $borderColour = $this->style->getBorderColourCode();
        } else {
            $borderColour = '';
        }

        return array_map(function ($row) use ($invertedColoursSetCode, $invertedColoursUnsetCode, $borderColour) {
            return sprintf(
                "%s%s%s%s%s%s%s%s%s%s%s%s\n",
                str_repeat(' ', $this->style->getMargin()),
                $borderColour,
                str_repeat(' ', $this->style->getBorderLeftWidth()),
                $this->style->getColoursSetCode(),
                $invertedColoursSetCode,
                str_repeat(' ', $this->style->getPaddingLeftRight()),
                $row,
                str_repeat(' ', $this->style->getRightHandPadding(mb_strlen(s::stripAnsiEscapeSequence($row)))),
                $invertedColoursUnsetCode,
                $borderColour,
                str_repeat(' ', $this->style->getBorderRightWidth()),
                $this->style->getColoursResetCode()
            );
        }, $rows);
    }

    /**
     * @throws InvalidTerminalException
     */
    public function open() : void
    {
        if ($this->isOpen()) {
            return;
        }

        if (count($this->items) === 0) {
            throw new \RuntimeException('Menu must have at least 1 item before it can be opened');
        }

        $this->configureTerminal();
        $this->open = true;
        $this->display();
    }

    /**
     * Close the menu
     *
     * @throws InvalidTerminalException
     */
    public function close() : void
    {
        $menu = $this;

        do {
            $menu->closeThis();
            $menu = $menu->getParent();
        } while (null !== $menu);

        $this->tearDownTerminal();
    }

    public function closeThis() : void
    {
        $this->terminal->clean();
        $this->terminal->moveCursorToTop();
        $this->open = false;
    }

    /**
     * @return MenuItemInterface[]
     */
    public function getItems() : array
    {
        return $this->items;
    }

    public function removeItem(MenuItemInterface $item) : void
    {
        $key = array_search($item, $this->items, true);

        if (false === $key) {
            throw new \InvalidArgumentException('Item does not exist in menu');
        }

        unset($this->items[$key]);
        $this->items = array_values($this->items);

        if ($this->selectedItem === $key) {
            $this->selectedItem = null;
            $this->selectFirstItem();
        }
    }

    public function getStyle() : MenuStyle
    {
        return $this->style;
    }

    public function setStyle(MenuStyle $style) : void
    {
        $this->style = $style;
    }

    public function getCurrentFrame() : Frame
    {
        return $this->currentFrame;
    }

    public function flash(string $text, MenuStyle $style = null) : Flash
    {
        $this->guardSingleLine($text);

        $style = $style ?? (new MenuStyle($this->terminal))
            ->setBg('yellow')
            ->setFg('red');

        return new Flash($this, $style, $this->terminal, $text);
    }

    public function confirm(string $text, MenuStyle $style = null) : Confirm
    {
        $this->guardSingleLine($text);

        $style = $style ?? (new MenuStyle($this->terminal))
            ->setBg('yellow')
            ->setFg('red');

        return new Confirm($this, $style, $this->terminal, $text);
    }

    public function askNumber(MenuStyle $style = null) : Number
    {
        $this->assertOpen();

        $style = $style ?? (new MenuStyle($this->terminal))
            ->setBg('yellow')
            ->setFg('red');

        return new Number(new InputIO($this, $this->terminal), $style);
    }

    public function askText(MenuStyle $style = null) : Text
    {
        $this->assertOpen();

        $style = $style ?? (new MenuStyle($this->terminal))
            ->setBg('yellow')
            ->setFg('red');

        return new Text(new InputIO($this, $this->terminal), $style);
    }

    public function askPassword(MenuStyle $style = null) : Password
    {
        $this->assertOpen();

        $style = $style ?? (new MenuStyle($this->terminal))
            ->setBg('yellow')
            ->setFg('red');

        return new Password(new InputIO($this, $this->terminal), $style);
    }

    private function guardSingleLine($text) : void
    {
        if (strpos($text, "\n") !== false) {
            throw new \InvalidArgumentException;
        }
    }
}


<?php

namespace PhpSchool\CliMenu\MenuItem;

use Assert\Assertion;
use PhpSchool\CliMenu\MenuStyle;
use PhpSchool\CliMenu\Util\StringUtil;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class SplitItem implements MenuItemInterface
{
    /**
     * @var array
     */
    private $items = [];

    /**
     * @var int|null
     */
    private $selectedItemIndex;

    /**
     * @var bool
     */
    private $canBeSelected = true;

    /**
     * @var int
     */
    private $gutter = 2;

    /**
     * @var array
     */
    private static $blacklistedItems = [
        \PhpSchool\CliMenu\MenuItem\AsciiArtItem::class,
        \PhpSchool\CliMenu\MenuItem\LineBreakItem::class,
        \PhpSchool\CliMenu\MenuItem\SplitItem::class,
    ];

    public function __construct(array $items = [])
    {
        $this->addItems($items);
        $this->setDefaultSelectedItem();
    }

    public function setGutter(int $gutter) : void
    {
        Assertion::greaterOrEqualThan($gutter, 0);
        $this->gutter = $gutter;
    }

    public function addItem(MenuItemInterface $item) : self
    {
        foreach (self::$blacklistedItems as $bl) {
            if ($item instanceof $bl) {
                throw new \InvalidArgumentException("Cannot add a $bl to a SplitItem");
            }
        }
        $this->items[] = $item;
        $this->setDefaultSelectedItem();
        return $this;
    }

    public function addItems(array $items) : self
    {
        foreach ($items as $item) {
            $this->addItem($item);
        }

        return $this;
    }

    public function setItems(array $items) : self
    {
        $this->items = [];
        $this->addItems($items);
        return $this;
    }

    /**
     * Select default item
     */
    private function setDefaultSelectedItem() : void
    {
        foreach ($this->items as $index => $item) {
            if ($item->canSelect()) {
                $this->canBeSelected = true;
                $this->selectedItemIndex = $index;
                return;
            }
        }

        $this->canBeSelected = false;
        $this->selectedItemIndex = null;
    }

    /**
     * The output text for the item
     */
    public function getRows(MenuStyle $style, bool $selected = false) : array
    {
        $numberOfItems = count($this->items);

        if ($numberOfItems === 0) {
            throw new \RuntimeException(sprintf('There should be at least one item added to: %s', __CLASS__));
        }

        if (!$selected) {
            $this->setDefaultSelectedItem();
        }

        $length = $style->getDisplaysExtra()
            ? floor($style->getContentWidth() / $numberOfItems) - (mb_strlen($style->getItemExtra()) + 2)
            : floor($style->getContentWidth() / $numberOfItems);

        $length -= $this->gutter;
        $length = (int) $length;

        $missingLength = $style->getContentWidth() % $numberOfItems;

        return $this->buildRows(
            array_map(function ($index, $item) use ($selected, $length, $style) {
                $isSelected = $selected && $index === $this->selectedItemIndex;
                $marker = $item->canSelect()
                    ? sprintf('%s', $style->getMarker($isSelected))
                    : '';

                $itemExtra = '';
                if ($style->getDisplaysExtra()) {
                    $itemExtra = $item->showsItemExtra()
                        ? sprintf('  %s', $style->getItemExtra())
                        : sprintf('  %s', str_repeat(' ', mb_strlen($style->getItemExtra())));
                }

                return $this->buildCell(
                    explode(
                        "\n",
                        StringUtil::wordwrap(
                            sprintf('%s%s', $marker, $item->getText()),
                            $length,
                            sprintf("\n%s", str_repeat(' ', mb_strlen($marker)))
                        )
                    ),
                    $length,
                    $style,
                    $isSelected,
                    $itemExtra
                );
            }, array_keys($this->items), $this->items),
            $style,
            $missingLength,
            $length
        );
    }

    private function buildRows(array $cells, MenuStyle $style, int $missingLength, int $length) : array
    {
        $extraPadLength = $style->getDisplaysExtra() ? 2 + mb_strlen($style->getItemExtra()) : 0;

        return array_map(
            function ($i) use ($cells, $length, $missingLength, $extraPadLength) {
                return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength);
            },
            range(0, max(array_map('count', $cells)) - 1)
        );
    }

    private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string
    {
        return sprintf(
            '%s%s',
            implode(
                '',
                array_map(
                    function ($cell) use ($index, $length, $extraPadLength) {
                        return $cell[$index] ?? str_repeat(' ', $length + $this->gutter + $extraPadLength);
                    },
                    $cells
                )
            ),
            str_repeat(' ', $missingLength)
        );
    }

    private function buildCell(
        array $content,
        int $length,
        MenuStyle $style,
        bool $isSelected,
        string $itemExtra
    ) : array {
        return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) {
            $invertedColoursSetCode = $isSelected
                ? $style->getInvertedColoursSetCode()
                : '';
            $invertedColoursUnsetCode = $isSelected
                ? $style->getInvertedColoursUnsetCode()
                : '';

            return sprintf(
                '%s%s%s%s%s%s',
                $invertedColoursSetCode,
                $row,
                str_repeat(' ', $length - mb_strlen($row)),
                $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)),
                $invertedColoursUnsetCode,
                str_repeat(' ', $this->gutter)
            );
        }, $content, array_keys($content));
    }

    /**
     * Is there an item with this index and can it be
     * selected?
     */
    public function canSelectIndex(int $index) : bool
    {
        return isset($this->items[$index]) && $this->items[$index]->canSelect();
    }

    /**
     * Set the item index which should be selected. If the item does
     * not exist then throw an exception.
     */
    public function setSelectedItemIndex(int $index) : void
    {
        if (!isset($this->items[$index])) {
            throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index));
        }

        $this->selectedItemIndex = $index;
    }

    /**
     * Get the currently select item index.
     * May be null in case of no selectable item.
     */
    public function getSelectedItemIndex() : ?int
    {
        return $this->selectedItemIndex;
    }

    /**
     * Get the currently selected item - if no items are selectable
     * then throw an exception.
     */
    public function getSelectedItem() : MenuItemInterface
    {
        if (null === $this->selectedItemIndex) {
            throw new \RuntimeException('No item is selected');
        }

        return $this->items[$this->selectedItemIndex];
    }

    public function getItems() : array
    {
        return $this->items;
    }

    /**
     * Can the item be selected
     * In this case, it indicates if at least 1 item inside the SplitItem can be selected
     */
    public function canSelect() : bool
    {
        return $this->canBeSelected;
    }

    /**
     * Execute the items callable if required
     */
    public function getSelectAction() : ?callable
    {
        return null;
    }

    /**
     * Whether or not the menu item is showing the menustyle extra value
     */
    public function showsItemExtra() : bool
    {
        return false;
    }

    /**
     * Enable showing item extra
     */
    public function showItemExtra() : void
    {
        //noop
    }

    /**
     * Disable showing item extra
     */
    public function hideItemExtra() : void
    {
        //noop
    }

    /**
     * Nothing to return with SplitItem
     */
    public function getText() : string
    {
        throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__));
    }
}


<?php

namespace PhpSchool\CliMenu\MenuItem;

use Assert\Assertion;
use PhpSchool\CliMenu\MenuStyle;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class LineBreakItem implements MenuItemInterface
{
    /**
     * @var string
     */
    private $breakChar;

    /**
     * @var int
     */
    private $lines;

    public function __construct(string $breakChar = ' ', int $lines = 1)
    {
        $this->breakChar = $breakChar;
        $this->lines     = $lines;
    }

    /**
     * The output text for the item
     */
    public function getRows(MenuStyle $style, bool $selected = false) : array
    {
        return explode(
            "\n",
            rtrim(str_repeat(sprintf(
                "%s\n",
                mb_substr(str_repeat($this->breakChar, $style->getContentWidth()), 0, $style->getContentWidth())
            ), $this->lines))
        );
    }

    /**
     * Can the item be selected
     */
    public function canSelect() : bool
    {
        return false;
    }

    /**
     * Execute the items callable if required
     */
    public function getSelectAction() : ?callable
    {
        return null;
    }

    /**
     * Return the raw string of text
     */
    public function getText() : string
    {
        return $this->breakChar;
    }

    /**
     * Set the raw string of text
     */
    public function setText(string $text) : void
    {
        $this->breakChar = $text;
    }

    /**
     * Whether or not the menu item is showing the menustyle extra value
     */
    public function showsItemExtra() : bool
    {
        return false;
    }

    /**
     * Enable showing item extra
     */
    public function showItemExtra() : void
    {
        //noop
    }

    /**
     * Disable showing item extra
     */
    public function hideItemExtra() : void
    {
        //noop
    }
}


<?php

namespace PhpSchool\CliMenu\Util;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class StringUtil
{
    /**
     * Minimal multi-byte wordwrap implementation
     * which also takes break length into consideration
     */
    public static function wordwrap(string $string, int $width, string $break = "\n") : string
    {
        return implode(
            $break,
            array_map(function (string $line) use ($width, $break) {
                $line = rtrim($line);
                if (mb_strlen($line) <= $width) {
                    return $line;
                }

                $words  = explode(' ', $line);
                $line   = '';
                $actual = '';
                foreach ($words as $word) {
                    if (mb_strlen($actual . $word) <= $width) {
                        $actual .= $word . ' ';
                    } else {
                        if ($actual !== '') {
                            $line .= rtrim($actual) . $break;
                        }
                        $actual = $word . ' ';
                    }
                }
                return $line . trim($actual);
            }, explode("\n", $string))
        );
    }

    public static function stripAnsiEscapeSequence(string $str) : string
    {
        return (string) preg_replace('/\x1b[^m]*m/', '', $str);
    }
}


<?php

namespace PhpSchool\CliMenu\MenuItem;

use PhpSchool\CliMenu\MenuStyle;
use PhpSchool\CliMenu\Util\StringUtil;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
trait SelectableTrait
{
    /**
     * @var string
     */
    private $text = '';

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

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

    /**
     * The output text for the item
     */
    public function getRows(MenuStyle $style, bool $selected = false) : array
    {
        $marker = sprintf("%s", $style->getMarker($selected));

        $length = $style->getDisplaysExtra()
            ? $style->getContentWidth() - (mb_strlen($style->getItemExtra()) + 2)
            : $style->getContentWidth();

        $rows = explode(
            "\n",
            StringUtil::wordwrap(
                sprintf('%s%s', $marker, $this->text),
                $length,
                sprintf("\n%s", str_repeat(' ', mb_strlen($marker)))
            )
        );

        return array_map(function ($row, $key) use ($style, $length) {
            $text = $this->disabled ? $style->getDisabledItemText($row) : $row;

            if ($key === 0) {
                return $this->showItemExtra
                    ? sprintf('%s%s  %s', $text, str_repeat(' ', $length - mb_strlen($row)), $style->getItemExtra())
                    : $text;
            }

            return $text;
        }, $rows, array_keys($rows));
    }

    /**
     * Can the item be selected
     */
    public function canSelect() : bool
    {
        return !$this->disabled;
    }

    public function showsItemExtra() : bool
    {
        return $this->showItemExtra;
    }

    /**
     * Enable showing item extra
     */
    public function showItemExtra() : void
    {
        $this->showItemExtra = true;
    }

    /**
     * Disable showing item extra
     */
    public function hideItemExtra() : void
    {
        $this->showItemExtra = false;
    }
}


<?php

namespace PhpSchool\CliMenu;

use PhpSchool\CliMenu\Terminal\TerminalFactory;
use PhpSchool\CliMenu\Util\ColourUtil;
use PhpSchool\Terminal\Terminal;
use Assert\Assertion;

//TODO: B/W fallback

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class MenuStyle
{
    /**
     * @var Terminal
     */
    protected $terminal;

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

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

    /**
     * @var int
     */
    protected $width;

    /**
     * @var int
     */
    protected $margin;

    /**
     * @var int
     */
    protected $paddingTopBottom;

    /**
     * @var int
     */
    protected $paddingLeftRight;

    /**
     * @var array
     */
    private $paddingTopBottomRows = [];

    /**
     * @var int
     */
    protected $contentWidth;

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

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

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

    /**
     * @var bool
     */
    private $displaysExtra;

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

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

    /**
     * @var string
     */
    private $invertedColoursSetCode = "\033[7m";

    /**
     * @var string
     */
    private $invertedColoursUnsetCode = "\033[27m";

    /**
     * @var string
     */
    private $coloursResetCode = "\033[0m";

    /**
     * @var int
     */
    private $borderTopWidth;

    /**
     * @var int
     */
    private $borderRightWidth;

    /**
     * @var int
     */
    private $borderBottomWidth;

    /**
     * @var int
     */
    private $borderLeftWidth;

    /**
     * @var string
     */
    private $borderColour = 'white';

    /**
     * @var array
     */
    private $borderTopRows = [];

    /**
     * @var array
     */
    private $borderBottomRows = [];

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

    /**
     * Default Values
     *
     * @var array
     */
    private static $defaultStyleValues = [
        'fg' => 'white',
        'bg' => 'blue',
        'width' => 100,
        'paddingTopBottom' => 1,
        'paddingLeftRight' => 2,
        'margin' => 2,
        'selectedMarker' => '● ',
        'unselectedMarker' => '○ ',
        'itemExtra' => '✔',
        'displaysExtra' => false,
        'titleSeparator' => '=',
        'borderTopWidth' => 0,
        'borderRightWidth' => 0,
        'borderBottomWidth' => 0,
        'borderLeftWidth' => 0,
        'borderColour' => 'white',
        'marginAuto' => false,
    ];

    /**
     * @var array
     */
    private static $availableForegroundColors = array(
        'black'   => 30,
        'red'     => 31,
        'green'   => 32,
        'yellow'  => 33,
        'blue'    => 34,
        'magenta' => 35,
        'cyan'    => 36,
        'white'   => 37,
        'default' => 39,
    );

    /**
     * @var array
     */
    private static $availableBackgroundColors = array(
        'black'   => 40,
        'red'     => 41,
        'green'   => 42,
        'yellow'  => 43,
        'blue'    => 44,
        'magenta' => 45,
        'cyan'    => 46,
        'white'   => 47,
        'default' => 49,
    );

    /**
     * @var array
     */
    private static $availableOptions = array(
        'bold'       => array('set' => 1, 'unset' => 22),
        'dim'        => array('set' => 2, 'unset' => 22),
        'underscore' => array('set' => 4, 'unset' => 24),
        'blink'      => array('set' => 5, 'unset' => 25),
        'reverse'    => array('set' => 7, 'unset' => 27),
        'conceal'    => array('set' => 8, 'unset' => 28)
    );

    /**
     * Initialise style
     */
    public function __construct(Terminal $terminal = null)
    {
        $this->terminal = $terminal ?: TerminalFactory::fromSystem();

        $this->fg = self::$defaultStyleValues['fg'];
        $this->bg = self::$defaultStyleValues['bg'];

        $this->generateColoursSetCode();

        $this->setWidth(self::$defaultStyleValues['width']);
        $this->setPaddingTopBottom(self::$defaultStyleValues['paddingTopBottom']);
        $this->setPaddingLeftRight(self::$defaultStyleValues['paddingLeftRight']);
        $this->setMargin(self::$defaultStyleValues['margin']);
        $this->setSelectedMarker(self::$defaultStyleValues['selectedMarker']);
        $this->setUnselectedMarker(self::$defaultStyleValues['unselectedMarker']);
        $this->setItemExtra(self::$defaultStyleValues['itemExtra']);
        $this->setDisplaysExtra(self::$defaultStyleValues['displaysExtra']);
        $this->setTitleSeparator(self::$defaultStyleValues['titleSeparator']);
        $this->setBorderTopWidth(self::$defaultStyleValues['borderTopWidth']);
        $this->setBorderRightWidth(self::$defaultStyleValues['borderRightWidth']);
        $this->setBorderBottomWidth(self::$defaultStyleValues['borderBottomWidth']);
        $this->setBorderLeftWidth(self::$defaultStyleValues['borderLeftWidth']);
        $this->setBorderColour(self::$defaultStyleValues['borderColour']);
    }

    public function hasChangedFromDefaults() : bool
    {
        $currentValues = [
            $this->fg,
            $this->bg,
            $this->width,
            $this->paddingTopBottom,
            $this->paddingLeftRight,
            $this->margin,
            $this->selectedMarker,
            $this->unselectedMarker,
            $this->itemExtra,
            $this->displaysExtra,
            $this->titleSeparator,
            $this->borderTopWidth,
            $this->borderRightWidth,
            $this->borderBottomWidth,
            $this->borderLeftWidth,
            $this->borderColour,
            $this->marginAuto,
        ];

        return $currentValues !== array_values(self::$defaultStyleValues);
    }

    public function getDisabledItemText(string $text) : string
    {
        return sprintf(
            "\033[%sm%s\033[%sm",
            self::$availableOptions['dim']['set'],
            $text,
            self::$availableOptions['dim']['unset']
        );
    }

    /**
     * Generates the ansi escape sequence to set the colours
     */
    private function generateColoursSetCode() : void
    {
        if (!ctype_digit($this->fg)) {
            $fgCode = self::$availableForegroundColors[$this->fg];
        } else {
            $fgCode = sprintf("38;5;%s", $this->fg);
        }

        if (!ctype_digit($this->bg)) {
            $bgCode = self::$availableBackgroundColors[$this->bg];
        } else {
            $bgCode = sprintf("48;5;%s", $this->bg);
        }

        $this->coloursSetCode = sprintf("\033[%s;%sm", $fgCode, $bgCode);
    }

    /**
     * Get the colour code for Bg and Fg
     */
    public function getColoursSetCode() : string
    {
        return $this->coloursSetCode;
    }

    /**
     * Get the inverted escape sequence (used for selected elements)
     */
    public function getInvertedColoursSetCode() : string
    {
        return $this->invertedColoursSetCode;
    }

    /**
     * Get the inverted escape sequence (used for selected elements)
     */
    public function getInvertedColoursUnsetCode() : string
    {
        return $this->invertedColoursUnsetCode;
    }

    /**
     * Get the escape sequence used to reset colours to default
     */
    public function getColoursResetCode() : string
    {
        return $this->coloursResetCode;
    }

    /**
     * Calculate the contents width
     */
    protected function calculateContentWidth() : void
    {
        $this->contentWidth = $this->width
            - ($this->paddingLeftRight * 2)
            - ($this->borderRightWidth + $this->borderLeftWidth);

        if ($this->contentWidth < 0) {
            $this->contentWidth = 0;
        }
    }

    public function getFg()
    {
        return $this->fg;
    }

    public function setFg(string $fg, string $fallback = null) : self
    {
        $this->fg = ColourUtil::validateColour(
            $this->terminal,
            $fg,
            $fallback
        );
        $this->generateColoursSetCode();

        return $this;
    }

    public function getBg()
    {
        return $this->bg;
    }

    public function setBg(string $bg, string $fallback = null) : self
    {
        $this->bg = ColourUtil::validateColour(
            $this->terminal,
            $bg,
            $fallback
        );

        $this->generateColoursSetCode();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function getWidth() : int
    {
        return $this->width;
    }

    public function setWidth(int $width) : self
    {
        Assertion::greaterOrEqualThan($width, 0);

        if ($width >= $this->terminal->getWidth()) {
            $width = $this->terminal->getWidth();
        }

        $this->width = $width;
        if ($this->marginAuto) {
            $this->setMarginAuto();
        }

        $this->calculateContentWidth();
        $this->generateBorderRows();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function getPaddingTopBottom() : int
    {
        return $this->paddingTopBottom;
    }

    public function getPaddingLeftRight() : int
    {
        return $this->paddingLeftRight;
    }

    private function generatePaddingTopBottomRows() : void
    {
        if ($this->borderLeftWidth || $this->borderRightWidth) {
            $borderColour = $this->getBorderColourCode();
        } else {
            $borderColour = '';
        }

        $paddingRow = sprintf(
            "%s%s%s%s%s%s%s%s%s%s\n",
            str_repeat(' ', $this->margin),
            $borderColour,
            str_repeat(' ', $this->borderLeftWidth),
            $this->getColoursSetCode(),
            str_repeat(' ', $this->paddingLeftRight),
            str_repeat(' ', $this->contentWidth),
            str_repeat(' ', $this->paddingLeftRight),
            $borderColour,
            str_repeat(' ', $this->borderRightWidth),
            $this->coloursResetCode
        );

        $this->paddingTopBottomRows = array_fill(0, $this->paddingTopBottom, $paddingRow);
    }

    public function getPaddingTopBottomRows() : array
    {
        return $this->paddingTopBottomRows;
    }

    public function setPadding(int $topBottom, int $leftRight = null) : self
    {
        if ($leftRight === null) {
            $leftRight = $topBottom;
        }

        $this->setPaddingTopBottom($topBottom);
        $this->setPaddingLeftRight($leftRight);

        $this->calculateContentWidth();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setPaddingTopBottom(int $topBottom) : self
    {
        Assertion::greaterOrEqualThan($topBottom, 0);
        $this->paddingTopBottom = $topBottom;

        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setPaddingLeftRight(int $leftRight) : self
    {
        Assertion::greaterOrEqualThan($leftRight, 0);
        $this->paddingLeftRight = $leftRight;

        $this->calculateContentWidth();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function getMargin() : int
    {
        return $this->margin;
    }

    public function setMarginAuto() : self
    {
        $this->marginAuto = true;
        $this->margin = (int) floor(($this->terminal->getWidth() - $this->width) / 2);

        $this->generateBorderRows();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setMargin(int $margin) : self
    {
        Assertion::greaterOrEqualThan($margin, 0);

        $this->marginAuto = false;
        $this->margin = $margin;

        $this->generateBorderRows();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function getContentWidth() : int
    {
        return $this->contentWidth;
    }

    /**
     * Get padding for right had side of content
     */
    public function getRightHandPadding(int $contentLength) : int
    {
        $rightPadding = $this->getContentWidth() - $contentLength + $this->getPaddingLeftRight();

        if ($rightPadding < 0) {
            $rightPadding = 0;
        }

        return $rightPadding;
    }

    public function getSelectedMarker() : string
    {
        return $this->selectedMarker;
    }

    public function setSelectedMarker(string $marker) : self
    {
        $this->selectedMarker = $marker;

        return $this;
    }

    public function getUnselectedMarker() : string
    {
        return $this->unselectedMarker;
    }

    public function setUnselectedMarker(string $marker) : self
    {
        $this->unselectedMarker = $marker;

        return $this;
    }

    /**
     * Get the correct marker for the item
     */
    public function getMarker(bool $selected) : string
    {
        return $selected ? $this->selectedMarker : $this->unselectedMarker;
    }

    public function setItemExtra(string $itemExtra) : self
    {
        $this->itemExtra = $itemExtra;

        return $this;
    }

    public function getItemExtra() : string
    {
        return $this->itemExtra;
    }

    public function getDisplaysExtra() : bool
    {
        return $this->displaysExtra;
    }

    public function setDisplaysExtra(bool $displaysExtra) : self
    {
        $this->displaysExtra = $displaysExtra;

        return $this;
    }

    public function getTitleSeparator() : string
    {
        return $this->titleSeparator;
    }

    public function setTitleSeparator(string $actionSeparator) : self
    {
        $this->titleSeparator = $actionSeparator;

        return $this;
    }

    private function generateBorderRows() : void
    {
        $borderRow = sprintf(
            "%s%s%s%s\n",
            str_repeat(' ', $this->margin),
            $this->getBorderColourCode(),
            str_repeat(' ', $this->width),
            $this->coloursResetCode
        );

        $this->borderTopRows = array_fill(0, $this->borderTopWidth, $borderRow);
        $this->borderBottomRows = array_fill(0, $this->borderBottomWidth, $borderRow);
    }

    public function getBorderTopRows() : array
    {
        return $this->borderTopRows;
    }

    public function getBorderBottomRows() : array
    {
        return $this->borderBottomRows;
    }

    /**
     * Shorthand function to set all borders values at once
     */
    public function setBorder(
        int $topWidth,
        $rightWidth = null,
        $bottomWidth = null,
        $leftWidth = null,
        string $colour = null
    ) : self {
        if (!is_int($rightWidth)) {
            $colour = $rightWidth;
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
        } elseif (!is_int($bottomWidth)) {
            $colour = $bottomWidth;
            $bottomWidth = $topWidth;
            $leftWidth = $rightWidth;
        } elseif (!is_int($leftWidth)) {
            $colour = $leftWidth;
            $leftWidth = $rightWidth;
        }

        $this->borderTopWidth = $topWidth;
        $this->borderRightWidth = $rightWidth;
        $this->borderBottomWidth = $bottomWidth;
        $this->borderLeftWidth = $leftWidth;

        if (is_string($colour)) {
            $this->setBorderColour($colour);
        } elseif ($colour !== null) {
            throw new \InvalidArgumentException('Invalid colour');
        }

        $this->calculateContentWidth();
        $this->generateBorderRows();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setBorderTopWidth(int $width) : self
    {
        $this->borderTopWidth = $width;

        $this->generateBorderRows();

        return $this;
    }

    public function setBorderRightWidth(int $width) : self
    {
        $this->borderRightWidth = $width;
        $this->calculateContentWidth();

        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setBorderBottomWidth(int $width) : self
    {
        $this->borderBottomWidth = $width;

        $this->generateBorderRows();

        return $this;
    }

    public function setBorderLeftWidth(int $width) : self
    {
        $this->borderLeftWidth = $width;
        $this->calculateContentWidth();

        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function setBorderColour(string $colour, $fallback = null) : self
    {
        $this->borderColour = ColourUtil::validateColour(
            $this->terminal,
            $colour,
            $fallback
        );

        $this->generateBorderRows();
        $this->generatePaddingTopBottomRows();

        return $this;
    }

    public function getBorderTopWidth() : int
    {
        return $this->borderTopWidth;
    }

    public function getBorderRightWidth() : int
    {
        return $this->borderRightWidth;
    }

    public function getBorderBottomWidth() : int
    {
        return $this->borderBottomWidth;
    }

    public function getBorderLeftWidth() : int
    {
        return $this->borderLeftWidth;
    }

    public function getBorderColour() : string
    {
        return $this->borderColour;
    }

    public function getBorderColourCode() : string
    {
        if (!ctype_digit($this->borderColour)) {
            $borderColourCode = self::$availableBackgroundColors[$this->borderColour];
        } else {
            $borderColourCode = sprintf("48;5;%s", $this->borderColour);
        }

        return sprintf("\033[%sm", $borderColourCode);
    }
}


<?php

namespace PhpSchool\CliMenu\Dialogue;

use PhpSchool\Terminal\NonCanonicalReader;

/**
 * @author Aydin Hassan <aydin@hotmail.co.uk>
 */
class Flash extends Dialogue
{
    /**
     * Flash a message on top of the menu which
     * disappears on any keystroke.
     */
    public function display() : void
    {
        $this->assertMenuOpen();

        $this->terminal->moveCursorToRow($this->y);

        $this->emptyRow();

        $this->write(sprintf(
            "%s%s%s%s%s\n",
            $this->style->getColoursSetCode(),
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            $this->text,
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            $this->style->getColoursResetCode()
        ));

        $this->emptyRow();

        $this->terminal->moveCursorToTop();

        $reader = new NonCanonicalReader($this->terminal);
        $reader->readCharacter();

        $this->parentMenu->redraw();
    }
}


<?php

namespace PhpSchool\CliMenu\Dialogue;

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\Exception\MenuNotOpenException;
use PhpSchool\CliMenu\MenuStyle;
use PhpSchool\Terminal\Terminal;

/**
 * @author Aydin Hassan <aydin@hotmail.co.uk>
 */
abstract class Dialogue
{
    /**
     * @var MenuStyle
     */
    protected $style;

    /**
     * @var CliMenu
     */
    protected $parentMenu;

    /**
     * @var Terminal
     */
    protected $terminal;

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

    /**
     * @var int
     */
    protected $x;

    /**
     * @var int
     */
    protected $y;

    public function __construct(CliMenu $parentMenu, MenuStyle $menuStyle, Terminal $terminal, string $text)
    {
        $this->style        = $menuStyle;
        $this->terminal     = $terminal;
        $this->text         = $text;
        $this->parentMenu   = $parentMenu;

        $this->calculateCoordinates();
    }

    /**
     * @throws MenuNotOpenException
     */
    protected function assertMenuOpen() : void
    {
        if (!$this->parentMenu->isOpen()) {
            throw new MenuNotOpenException;
        }
    }

    /**
     * Calculate the co-ordinates to write the messages
     */
    protected function calculateCoordinates() : void
    {
        //y
        $textLines = count(explode("\n", $this->text)) + 2;
        $this->y = (int) (ceil($this->parentMenu->getCurrentFrame()->count() / 2) - ceil($textLines / 2) + 1);

        //x
        $parentStyle = $this->parentMenu->getStyle();
        $dialogueHalfLength = (int) ((mb_strlen($this->text) + ($this->style->getPaddingLeftRight() * 2)) / 2);
        $widthHalfLength = (int) ceil($parentStyle->getWidth() / 2 + $parentStyle->getMargin());
        $this->x = $widthHalfLength - $dialogueHalfLength;
    }

    /**
     * Write an empty row
     */
    protected function emptyRow() : void
    {
        $this->write(
            sprintf(
                "%s%s%s%s%s\n",
                $this->style->getColoursSetCode(),
                str_repeat(' ', $this->style->getPaddingLeftRight()),
                str_repeat(' ', mb_strlen($this->text)),
                str_repeat(' ', $this->style->getPaddingLeftRight()),
                $this->style->getColoursResetCode()
            )
        );
    }

    /**
     * Write some text at a particular column
     */
    protected function write(string $text, int $column = null) : void
    {
        $this->terminal->moveCursorToColumn($column ?: $this->x);
        $this->terminal->write($text);
    }

    public function getStyle() : MenuStyle
    {
        return $this->style;
    }
}


<?php

namespace PhpSchool\CliMenu\MenuItem;

use Assert\Assertion;
use PhpSchool\CliMenu\MenuStyle;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class AsciiArtItem implements MenuItemInterface
{
    /**
     * Possible positions of the ascii art
     */
    const POSITION_CENTER = 'center';
    const POSITION_LEFT   = 'left';
    const POSITION_RIGHT  = 'right';

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

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

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

    /**
     * @var int
     */
    private $artLength;

    public function __construct(string $text, string $position = self::POSITION_CENTER, string $alt = '')
    {
        Assertion::inArray($position, [self::POSITION_CENTER, self::POSITION_RIGHT, self::POSITION_LEFT]);

        $this->setText($text);
        $this->position  = $position;
        $this->alternateText = $alt;
    }

    /**
     * The output text for the item
     */
    public function getRows(MenuStyle $style, bool $selected = false) : array
    {
        if ($this->artLength > $style->getContentWidth()) {
            $alternate = new StaticItem($this->alternateText);
            return $alternate->getRows($style, false);
        }

        $padding = $style->getContentWidth() - $this->artLength;

        return array_map(function ($row) use ($padding) {
            switch ($this->position) {
                case self::POSITION_LEFT:
                    break;
                case self::POSITION_RIGHT:
                    $row = sprintf('%s%s', str_repeat(' ', $padding), $row);
                    break;
                case self::POSITION_CENTER:
                default:
                    $left = (int) ceil($padding / 2);
                    $row = sprintf('%s%s', str_repeat(' ', $left), $row);
                    break;
            }

            return $row;
        }, explode("\n", $this->text));
    }

    /**
     * Can the item be selected
     */
    public function canSelect() : bool
    {
        return false;
    }

    /**
     * Execute the items callable if required
     */
    public function getSelectAction() : ?callable
    {
        return null;
    }

    /**
     * Return the raw string of text
     */
    public function getText() : string
    {
        return $this->text;
    }

    /**
     * Set the raw string of text
     */
    public function setText(string $text) : void
    {
        $this->text = implode("\n", array_map(function (string $line) {
            return rtrim($line, ' ');
        }, explode("\n", $text)));

        $this->calculateArtLength();
    }

    /**
     * Calculate the length of the art
     */
    private function calculateArtLength() : void
    {
        $this->artLength = (int) max(array_map('mb_strlen', explode("\n", $this->text)));
    }

    /**
     * Return the length of the art
     */
    public function getArtLength() : int
    {
        return $this->artLength;
    }

    /**
     * Whether or not the menu item is showing the menustyle extra value
     */
    public function showsItemExtra() : bool
    {
        return false;
    }

    /**
     * Enable showing item extra
     */
    public function showItemExtra() : void
    {
        //noop
    }

    /**
     * Disable showing item extra
     */
    public function hideItemExtra() : void
    {
        //noop
    }
}


<?php

namespace PhpSchool\CliMenu\Util;

use Assert\Assertion;
use PhpSchool\Terminal\Terminal;

class ColourUtil
{
    /**
     * @var array
     */
    private static $defaultColoursNames = [
        'black',
        'red',
        'green',
        'yellow',
        'blue',
        'magenta',
        'cyan',
        'white',
        'default',
    ];

    /**
     * @var array
     */
    private static $coloursMap = [
        0 => 'black',
        1 => 'red',
        2 => 'green',
        3 => 'yellow',
        4 => 'blue',
        5 => 'magenta',
        6 => 'cyan',
        7 => 'white',
        8 => 'black',
        9 => 'red',
        10 => 'green',
        11 => 'yellow',
        12 => 'blue',
        13 => 'magenta',
        14 => 'cyan',
        15 => 'white',
        16 => 'black',
        17 => 'blue',
        18 => 'blue',
        19 => 'blue',
        20 => 'blue',
        21 => 'blue',
        22 => 'green',
        23 => 'cyan',
        24 => 'cyan',
        25 => 'blue',
        26 => 'blue',
        27 => 'blue',
        28 => 'green',
        29 => 'green',
        30 => 'cyan',
        31 => 'cyan',
        32 => 'blue',
        33 => 'blue',
        34 => 'green',
        35 => 'green',
        36 => 'green',
        37 => 'cyan',
        38 => 'cyan',
        39 => 'cyan',
        40 => 'green',
        41 => 'green',
        42 => 'green',
        43 => 'cyan',
        44 => 'cyan',
        45 => 'cyan',
        46 => 'green',
        47 => 'green',
        48 => 'green',
        49 => 'green',
        50 => 'cyan',
        51 => 'cyan',
        52 => 'red',
        53 => 'blue',
        54 => 'blue',
        55 => 'blue',
        56 => 'blue',
        57 => 'blue',
        58 => 'yellow',
        59 => 'black',
        60 => 'blue',
        61 => 'blue',
        62 => 'blue',
        63 => 'blue',
        64 => 'green',
        65 => 'green',
        66 => 'cyan',
        67 => 'cyan',
        68 => 'blue',
        69 => 'blue',
        70 => 'green',
        71 => 'green',
        72 => 'green',
        73 => 'cyan',
        74 => 'cyan',
        75 => 'cyan',
        76 => 'green',
        77 => 'green',
        78 => 'green',
        79 => 'green',
        80 => 'cyan',
        81 => 'cyan',
        82 => 'green',
        83 => 'green',
        84 => 'green',
        85 => 'green',
        86 => 'cyan',
        87 => 'cyan',
        88 => 'red',
        89 => 'magenta',
        90 => 'magenta',
        91 => 'magenta',
        92 => 'blue',
        93 => 'blue',
        94 => 'yellow',
        95 => 'red',
        96 => 'magenta',
        97 => 'magenta',
        98 => 'blue',
        99 => 'blue',
        100 => 'yellow',
        101 => 'yellow',
        102 => 'white',
        103 => 'blue',
        104 => 'blue',
        105 => 'blue',
        106 => 'green',
        107 => 'green',
        108 => 'green',
        109 => 'cyan',
        110 => 'cyan',
        111 => 'cyan',
        112 => 'green',
        113 => 'green',
        114 => 'green',
        115 => 'green',
        116 => 'cyan',
        117 => 'cyan',
        118 => 'green',
        119 => 'green',
        120 => 'green',
        121 => 'green',
        122 => 'green',
        123 => 'cyan',
        124 => 'red',
        125 => 'magenta',
        126 => 'magenta',
        127 => 'magenta',
        128 => 'magenta',
        129 => 'magenta',
        130 => 'red',
        131 => 'red',
        132 => 'magenta',
        133 => 'magenta',
        134 => 'magenta',
        135 => 'magenta',
        136 => 'yellow',
        137 => 'red',
        138 => 'red',
        139 => 'magenta',
        140 => 'magenta',
        141 => 'magenta',
        142 => 'yellow',
        143 => 'yellow',
        144 => 'yellow',
        145 => 'white',
        146 => 'white',
        147 => 'white',
        148 => 'yellow',
        149 => 'green',
        150 => 'green',
        151 => 'green',
        152 => 'cyan',
        153 => 'white',
        154 => 'green',
        155 => 'green',
        156 => 'green',
        157 => 'green',
        158 => 'green',
        159 => 'cyan',
        160 => 'red',
        161 => 'magenta',
        162 => 'magenta',
        163 => 'magenta',
        164 => 'magenta',
        165 => 'magenta',
        166 => 'red',
        167 => 'red',
        168 => 'magenta',
        169 => 'magenta',
        170 => 'magenta',
        171 => 'magenta',
        172 => 'red',
        173 => 'red',
        174 => 'red',
        175 => 'magenta',
        176 => 'magenta',
        177 => 'magenta',
        178 => 'yellow',
        179 => 'yellow',
        180 => 'white',
        181 => 'white',
        182 => 'magenta',
        183 => 'magenta',
        184 => 'yellow',
        185 => 'yellow',
        186 => 'yellow',
        187 => 'yellow',
        188 => 'white',
        189 => 'white',
        190 => 'yellow',
        191 => 'yellow',
        192 => 'green',
        193 => 'green',
        194 => 'green',
        195 => 'cyan',
        196 => 'red',
        197 => 'red',
        198 => 'magenta',
        199 => 'magenta',
        200 => 'magenta',
        201 => 'magenta',
        202 => 'red',
        203 => 'red',
        204 => 'magenta',
        205 => 'magenta',
        206 => 'magenta',
        207 => 'magenta',
        208 => 'red',
        209 => 'red',
        210 => 'red',
        211 => 'magenta',
        212 => 'magenta',
        213 => 'magenta',
        214 => 'red',
        215 => 'white',
        216 => 'red',
        217 => 'red',
        218 => 'magenta',
        219 => 'magenta',
        220 => 'yellow',
        221 => 'yellow',
        222 => 'yellow',
        223 => 'white',
        224 => 'white',
        225 => 'magenta',
        226 => 'yellow',
        227 => 'yellow',
        228 => 'yellow',
        229 => 'yellow',
        230 => 'yellow',
        231 => 'white',
        232 => 'black',
        233 => 'black',
        234 => 'black',
        235 => 'black',
        236 => 'black',
        237 => 'black',
        238 => 'black',
        239 => 'black',
        240 => 'black',
        241 => 'black',
        242 => 'black',
        243 => 'black',
        244 => 'white',
        245 => 'white',
        246 => 'white',
        247 => 'white',
        248 => 'white',
        249 => 'white',
        250 => 'white',
        251 => 'white',
        252 => 'white',
        253 => 'white',
        254 => 'white',
        255 => 'white',
    ];

    public static function getDefaultColourNames() : array
    {
        return self::$defaultColoursNames;
    }

    /**
     * Simple function to transform a 8-bit (256 colours) colour code
     * to one of the default 8 colors available in the terminal
     */
    public static function map256To8(int $colourCode) : string
    {
        if (!isset(self::$coloursMap[$colourCode])) {
            throw new \InvalidArgumentException('Invalid colour code');
        }

        return self::$coloursMap[$colourCode];
    }

    /**
     * Check if $colour exists
     * If it's a 256-colours code and $terminal doesn't support it, returns a fallback value
     */
    public static function validateColour(Terminal $terminal, string $colour, string $fallback = null) : string
    {
        if (!ctype_digit($colour)) {
            return static::validateColourName($colour);
        }

        Assertion::between($colour, 0, 255, 'Invalid colour code');

        if ($terminal->getColourSupport() >= 256) {
            return $colour;
        }

        if ($fallback !== null) {
            return static::validateColourName($fallback);
        }

        return static::map256To8((int) $colour);
    }

    private static function validateColourName(string $colourName) : string
    {
        Assertion::inArray($colourName, static::getDefaultColourNames());
        return $colourName;
    }
}


<?php

namespace PhpSchool\CliMenu\Dialogue;

use PhpSchool\Terminal\InputCharacter;
use PhpSchool\Terminal\NonCanonicalReader;

/**
 * @author Aydin Hassan <aydin@hotmail.co.uk>
 */
class Confirm extends Dialogue
{

    /**
     * Display confirmation with a button with the given text
     */
    public function display(string $confirmText = 'OK') : void
    {
        $this->assertMenuOpen();

        $this->terminal->moveCursorToRow($this->y);

        $promptWidth = mb_strlen($this->text) + 4;

        $this->emptyRow();

        $this->write(sprintf(
            "%s%s%s%s%s\n",
            $this->style->getColoursSetCode(),
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            $this->text,
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            $this->style->getColoursResetCode()
        ));

        $this->emptyRow();

        $confirmText = sprintf(' < %s > ', $confirmText);
        $leftFill    = (int) (($promptWidth / 2) - (mb_strlen($confirmText) / 2));

        $this->write(sprintf(
            "%s%s%s%s%s%s%s\n",
            $this->style->getColoursSetCode(),
            str_repeat(' ', $leftFill),
            $this->style->getInvertedColoursSetCode(),
            $confirmText,
            $this->style->getInvertedColoursUnsetCode(),
            str_repeat(' ', (int) ceil($promptWidth - $leftFill - mb_strlen($confirmText))),
            $this->style->getColoursResetCode()
        ));

        $this->write(sprintf(
            "%s%s%s%s%s\n",
            $this->style->getColoursSetCode(),
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            str_repeat(' ', mb_strlen($this->text)),
            str_repeat(' ', $this->style->getPaddingLeftRight()),
            $this->style->getColoursResetCode()
        ));

        $this->terminal->moveCursorToTop();

        $reader = new NonCanonicalReader($this->terminal);

        while ($char = $reader->readCharacter()) {
            if ($char->isControl() && $char->getControl() === InputCharacter::ENTER) {
                $this->parentMenu->redraw();
                return;
            }
        }
    }
}


<?php

namespace PhpSchool\CliMenu\MenuItem;

use Assert\Assertion;
use PhpSchool\CliMenu\MenuStyle;
use PhpSchool\CliMenu\Util\StringUtil;

/**
 * @author Michael Woodward <mikeymike.mw@gmail.com>
 */
class StaticItem implements MenuItemInterface
{
    /**
     * @var string
     */
    private $text;

    public function __construct(string $text)
    {
        $this->text = $text;
    }

    /**
     * The output text for the item
     */
    public function getRows(MenuStyle $style, bool $selected = false) : array
    {
        return explode("\n", StringUtil::wordwrap($this->text, $style->getContentWidth()));
    }

    /**
     * Can the item be selected
     */
    public function canSelect() : bool
    {
        return false;
    }

    /**
     * Execute the items callable if required
     */
    public function getSelectAction() : ?callable
    {
        return null;
    }

    /**
     * Return the raw string of text
     */
    public function getText() : string
    {
        return $this->text;
    }

    /**
     * Set the raw string of text
     */
    public function setText(string $text) : void
    {
        $this->text = $text;
    }

    /**
     * Whether or not the menu item is showing the menustyle extra value
     */
    public function showsItemExtra() : bool
    {
        return false;
    }

    /**
     * Enable showing item extra
     */
    public function showItemExtra() : void
    {
        //noop
    }

    /**
     * Disable showing item extra
     */
    public function hideItemExtra() : void
    {
        //noop
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/9/28 0028
 * Time: 下午 16:35
 */

namespace ChenPay;

use ChenPay\PayException\PayException;
use \GuzzleHttp\Exception\GuzzleException;

class AliPay extends Pay
{
    public $url = false;

    /**
     * @return $this
     * @throws PayException
     */
    public function getRefresh()
    {
        try {
            $aliPayHtml = (new \GuzzleHttp\Client())
                ->request('POST', "https://enterpriseportal.alipay.com/portal/navload.json?t=" . time() * 1000, [
                    'timeout' => 10,
                    'headers' => [
                        'Cookie' => $this->cookie,
                        'Accept-Encoding' => 'gzip, deflate, br',
                        'Accept-Language' => 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
                        'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
                        'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
                        'Accept' => 'application/json, text/javascript',
                        'Referer' => 'https://mbillexprod.alipay.com/enterprise/tradeListQuery.htm',
                        'Origin' => 'https://mbillexprod.alipay.com',
                        'Connection' => 'keep-alive',
                    ],
                    'body' => 'action=loadEntInfo'
                ])
                ->getBody();
        } catch (GuzzleException $e) {
            throw new PayException($e->getMessage(), 500);
        }
        if (!preg_match('/navResult/', $aliPayHtml)) throw new PayException('cookie失效', 445);
        return $this;
    }

    /**
     * @return \Psr\Http\Message\StreamInterface
     * @throws PayException
     */
    public function HtmlOne()
    {
        try {
            return (new \GuzzleHttp\Client())
                ->request('POST', "https://mbillexprod.alipay.com/enterprise/tradeListQuery.json", [
                    'timeout' => 10,
                    'headers' => [
                        'Cookie' => $this->cookie,
                        'Origin' => 'https://mbillexprod.alipay.com',
                        'Accept-Encoding' => 'gzip, deflate, br',
                        'Accept-Language' => 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
                        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
                        'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
                        'Accept' => 'application/json, text/javascript',
                        'Referer' => 'https://mbillexprod.alipay.com/enterprise/tradeListQuery.htm',
                        'X-Requested-With' => 'XMLHttpRequest',
                        'Connection' => 'keep-alive',
                    ],
                    'body' => 'queryEntrance=1&billUserId=' . Cookie::getCookieName('uid', $this->cookie) .
                        '&status=SUCCESS&entityFilterType=0&activeTargetSearchItem=tradeNo&tradeFrom=ALL&startTime=' .
                        date('Y-m-d', strtotime('-1 day')) . '+00%3A00%3A00&endTime=' . date('Y-m-d') .
                        '+23%3A59%3A59&pageSize=20&pageNum=1&total=1&sortTarget=gmtCreate&order=descend&sortType=0&_input_charset=gbk&ctoken=' .
                        Cookie::getCookieName('ctoken', $this->cookie),
                ])
                ->getBody();
        } catch (GuzzleException $e) {
            throw new PayException($e->getMessage(), 500);
        } catch (PayException $e) {
            throw new PayException($e->getMessage(), 445);
        }
    }

    /**
     * @return \Psr\Http\Message\StreamInterface
     * @throws PayException
     */
    public function HtmlTwo()
    {
        try {
            return (new \GuzzleHttp\Client())
                ->request('POST', "https://mbillexprod.alipay.com/enterprise/fundAccountDetail.json", [
                    'timeout' => 10,
                    'headers' => [
                        'Cookie' => $this->cookie,
                        'Origin' => 'https://mbillexprod.alipay.com',
                        'Accept-Encoding' => 'gzip, deflate, br',
                        'Accept-Language' => 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
                        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',
                        'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
                        'Accept' => 'application/json, text/javascript',
                        'Referer' => 'https://mbillexprod.alipay.com/enterprise/fundAccountDetail.htm',
                        'X-Requested-With' => 'XMLHttpRequest',
                        'Connection' => 'keep-alive',
                    ],

                    'body' => 'queryEntrance=1&billUserId=' . Cookie::getCookieName('uid', $this->cookie) .
                        '&showType=1&type=&precisionQueryKey=tradeNo&' .
                        'startDateInput=' . date('Y-m-d', strtotime('-1 day')) . '+00%3A00%3A00&endDateInput=' . date('Y-m-d') . '+23%3A59%3A59&' .
                        'pageSize=20&pageNum=1&total=1&sortTarget=tradeTime&order=descend&sortType=0&' .
                        '_input_charset=gbk&ctoken=' . Cookie::getCookieName('ctoken', $this->cookie)
                ])
                ->getBody();
        } catch (GuzzleException $e) {
            throw new PayException($e->getMessage(), 500);
        } catch (PayException $e) {
            throw new PayException($e->getMessage(), 445);
        }
    }

    /**
     * @param bool $url
     * @param bool $syncKey
     * @return $this
     * @throws PayException
     */
    public function getData($url = false, $syncKey = false)
    {
        // TODO: Implement getData() method.
        $this->url = $url;
        $this->getRefresh();
        $aliPayHtml = $url ? $this->HtmlOne()->getContents() : $this->HtmlTwo()->getContents();
        if (preg_match('/"failed"/', $aliPayHtml)) {
            $aliPayHtml = !$url ? $this->HtmlOne()->getContents() : $this->HtmlTwo()->getContents();
            if (preg_match('/"failed"/', $aliPayHtml)) throw new PayException('频繁访问', 446);
            $this->url = !$url;
        }
        try {
            $this->html = iconv('GBK', 'UTF-8', $aliPayHtml);
        } catch (\Exception $e) {
            throw new PayException('处理出错', 444);
        }
        return $this;
    }

    /**
     * @return $this
     * @throws PayException
     */
    public function DataHandle()
    {
        try {
            $this->json = json_decode($this->html, true);
        } catch (\Exception $e) {
            throw new PayException('解析出错', 444);
        }
//        if (isset($this->json['exception_marking'])) throw new PayException('数据出错', 444);
        if (isset($this->json['target'])) throw new PayException('cookie失效', 445);
        return $this;
    }

    /**
     * 获取最新的订单号
     * @param $fee
     * @param $time
     * @param int $Minute
     * @param bool $Remarks
     * @return array|bool
     */
    public function DataContrast($fee, $time, $Minute = 3, $Remarks = false)
    {
        // TODO: Implement DataContrast() method.
//        print_r($this->json['result']['detail']);
        if (isset($this->json['result']['detail']) && is_array($this->json['result']['detail']))
            foreach ($this->json['result']['detail'] as $item) {
                if ((
                        ($this->url && $item['tradeFrom'] == '外部商户' && $item['direction'] == '卖出' &&
                            strtotime($item['gmtCreate']) > $time - $Minute * 60 && strtotime($item['gmtCreate']) < $time &&
                            $item['totalAmount'] == $fee) ||
                        (!$this->url && $item['signProduct'] == '转账收款码' && $item['accountType'] == '交易' &&
                            strtotime($item['tradeTime']) > $time - $Minute * 60 && strtotime($item['tradeTime']) < $time &&
                            $item['tradeAmount'] == $fee)
                    ) && ($Remarks === false || ($Remarks != '' && (preg_match("/{$Remarks}/", $item['goodsTitle'])) ||
                            ($Remarks == '' && $item['goodsTitle'] == '商品')))) {
                    return $item['tradeNo'];
                }
            }

        return false;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/9/28 0028
 * Time: 下午 16:35
 */

namespace ChenPay;

use ChenPay\PayException\PayException;
use \GuzzleHttp\Exception\GuzzleException;

class WxPay extends Pay
{
    public $url = false;
    public $syncKey = false;

    /**
     * 微信心跳包
     * @return $this
     * @throws PayException
     */
    public function getSyncKey()
    {
        try {
            $html = (new \GuzzleHttp\Client())
                ->request('POST', "https://" . $this->url . "/cgi-bin/mmwebwx-bin/webwxinit?r=695888609", [
                    'timeout' => 10,
                    'headers' => [
                        'Accept' => 'application/json, text/javascript',
                        'Accept-Encoding' => 'gzip, deflate, br',
                        'Accept-Language' => 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
                        'Connection' => 'keep-alive',
                        'Content-Length' => '295',
                        'Content-Type' => 'application/json;charset=UTF-8',
                        'Cookie' => $this->cookie,
                        'Host' => $this->url,
                        'Origin' => 'https://' . $this->url,
                        'Referer' => 'https://' . $this->url . '/',
                        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
                    ],
                    'body' => '{"BaseRequest":{"Uin":' . Cookie::getCookieName('wxuin', $this->cookie) .
                        ',"Sid":"' . Cookie::getCookieName('wxsid', $this->cookie) . '","Skey":' .
                        '"","DeviceID":"e453731506754000"}}'
                ])
                ->getBody();
            return json_decode($html->getContents(), true);
        } catch (GuzzleException $e) {
            throw new PayException('访问出错', 500);
        } catch (PayException $e) {
            throw new PayException($e->getMessage(), $e->getCode());
        } catch (\Exception $e) {
            throw new PayException('处理出错', 444);
        }
    }

    /**
     * @param $url
     * @param $syncKey
     * @return $this
     * @throws PayException
     */
    public function getData($url, $syncKey = false)
    {
        // TODO: Implement getData() method.
        $this->url = $url;
        if (!$syncKey || preg_match('/"Count"\:0/', $syncKey)) {
            $syncJson = $this->getSyncKey();
            if ($syncJson['BaseResponse']['Ret'] > 0) throw new PayException('cookie失效', 445);
            $sync = json_encode($syncJson['SyncKey']);
        } else $sync = $syncKey;
        try {
            $html = (new \GuzzleHttp\Client())
                ->request('POST', "https://" . $this->url . "/cgi-bin/mmwebwx-bin/webwxsync?sid=" .
                    Cookie::getCookieName('wxsid', $this->cookie) . "&skey=", [
                    'timeout' => 10,
                    'headers' => [
                        'Accept' => 'application/json, text/javascript',
                        'Accept-Encoding' => 'gzip, deflate, br',
                        'Accept-Language' => 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
                        'Connection' => 'keep-alive',
                        'Content-Length' => '295',
                        'Content-Type' => 'application/json;charset=UTF-8',
                        'Cookie' => $this->cookie,
                        'Host' => $this->url,
                        'Origin' => 'https://' . $this->url,
                        'Referer' => 'https://' . $this->url . '/',
                        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
                    ],
                    'body' => '{"BaseRequest":{"Uin":' . Cookie::getCookieName('wxuin', $this->cookie) .
                        ',"Sid":"' . Cookie::getCookieName('wxsid', $this->cookie) . '","Skey":"' .
                        '","DeviceID":"e453731506754000"},"SyncKey":' . $sync .
                        ',"rr":' . rand(100000000, 999999999) . '}'
                ])
                ->getBody();
            $this->html = $html->getContents();
            $this->syncKey = json_encode(json_decode($this->html, true)['SyncKey']);
        } catch (GuzzleException $e) {
            throw new PayException($e->getMessage(), 500);
        } catch (PayException $e) {
            throw new PayException($e->getMessage(), $e->getCode());
        } catch (\Exception $e) {
            throw new PayException('处理出错', 444);
        }
        return $this;
    }

    /**
     * @return $this
     * @throws PayException
     */
    public function DataHandle()
    {
        try {
            $this->json = json_decode($this->html, true);
        } catch (\Exception $e) {
            throw new PayException('解析出错', 444);
        }
        if ($this->json['BaseResponse']['Ret'] > 0)
            throw new PayException('cookie失效', 445);
        return $this;
    }

    /**
     * 获取最新的订单号
     * @param $fee
     * @param $time
     * @param int $Minute
     * @param bool $Remarks
     * @return array|bool
     */
    public function DataContrast($fee, $time, $Minute = 3, $Remarks = false)
    {
        // TODO: Implement DataContrast() method.
        if (isset($this->json['AddMsgList']) && is_array($this->json['AddMsgList']))
            foreach ($this->json['AddMsgList'] as $item) {
                if (preg_match('/微信支付收款/', $item['FileName'])) {
                    $fees = explode('微信支付收款', $item['FileName']);
                    $fees = explode('元', $fees[1])[0];
                    if ($item['CreateTime'] < $time && $item['CreateTime'] > $time - $Minute * 60 &&
                        $fees == $fee && ($Remarks === false || (($Remarks != '' && preg_match("/备注：{$Remarks}</", $item['Content']))
                                || ($Remarks == '' && !preg_match("/备注：/", $item['Content'])))
                        )) {
                        return $item['MsgId'];
                    }
                }
            }
        return false;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/9/28 0028
 * Time: 下午 16:44
 */

namespace ChenPay;

use ChenPay\PayException\PayException;

class Cookie
{
    /**
     * 获取cookie某key值
     * @param string $name
     * @param bool $cookie
     * @return mixed
     * @throws PayException
     */
    public static function getCookieName($name = 'uid', $cookie = false)
    {
        $getCookie = explode($name . '=', $cookie);
        if (count($getCookie) <= 1) throw new PayException('cookie有误', 445);
        if ($name == 'uid') return explode('"', $getCookie[1])[0];
        else return explode(';', $getCookie[1])[0];
    }
}


<?php namespace Cviebrock\LaravelElasticsearch;

use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

class Factory
{

    /**
     * Map configuration array keys with ES ClientBuilder setters
     *
     * @var array
     */
    protected $configMappings = [
        'sslVerification'    => 'setSSLVerification',
        'sniffOnStart'       => 'setSniffOnStart',
        'retries'            => 'setRetries',
        'httpHandler'        => 'setHandler',
        'connectionPool'     => 'setConnectionPool',
        'connectionSelector' => 'setSelector',
        'serializer'         => 'setSerializer',
        'connectionFactory'  => 'setConnectionFactory',
        'endpoint'           => 'setEndpoint',
        'namespaces'         => 'registerNamespace',
    ];

    /**
     * Make the Elasticsearch client for the given named configuration, or
     * the default client.
     *
     * @param array $config
     *
     * @return \Elasticsearch\Client|mixed
     */
    public function make(array $config)
    {
        // Build the client
        return $this->buildClient($config);
    }

    /**
     * Build and configure an Elasticsearch client.
     *
     * @param array $config
     *
     * @return \Elasticsearch\Client
     */
    protected function buildClient(array $config): Client
    {
        $clientBuilder = ClientBuilder::create();

        // Configure hosts

        $clientBuilder->setHosts($config['hosts']);

        // Configure logging

        if (array_get($config, 'logging')) {
            $logObject = array_get($config, 'logObject');
            $logPath = array_get($config, 'logPath');
            $logLevel = array_get($config, 'logLevel');
            if ($logObject && $logObject instanceof LoggerInterface) {
                $clientBuilder->setLogger($logObject);
            } elseif ($logPath && $logLevel) {
                $handler = new StreamHandler($logPath, $logLevel);
                $logObject = new Logger('log');
                $logObject->pushHandler($handler);
                $clientBuilder->setLogger($logObject);
            }
        }

        // Set additional client configuration

        foreach ($this->configMappings as $key => $method) {
            $value = array_get($config, $key);
            if (is_array($value)) {
                foreach ($value as $vItem) {
                    $clientBuilder->$method($vItem);
                }
            } elseif ($value !== null) {
                $clientBuilder->$method($value);
            }
        }

        // Configure handlers for any AWS hosts

        foreach ($config['hosts'] as $host) {
            if (isset($host['aws']) && $host['aws']) {
                $clientBuilder->setHandler(function(array $request) use ($host) {
                    $psr7Handler = \Aws\default_http_handler();
                    $signer = new \Aws\Signature\SignatureV4('es', $host['aws_region']);
                    $request['headers']['Host'][0] = parse_url($request['headers']['Host'][0])['host'];

                    // Create a PSR-7 request from the array passed to the handler
                    $psr7Request = new \GuzzleHttp\Psr7\Request(
                        $request['http_method'],
                        (new \GuzzleHttp\Psr7\Uri($request['uri']))
                            ->withScheme($request['scheme'])
                            ->withHost($request['headers']['Host'][0]),
                        $request['headers'],
                        $request['body']
                    );

                    // Sign the PSR-7 request with credentials from the environment
                    $signedRequest = $signer->signRequest(
                        $psr7Request,
                        new \Aws\Credentials\Credentials($host['aws_key'], $host['aws_secret'])
                    );

                    // Send the signed request to Amazon ES
                    /** @var \Psr\Http\Message\ResponseInterface $response */
                    $response = $psr7Handler($signedRequest)
                        ->then(function(\Psr\Http\Message\ResponseInterface $response) {
                            return $response;
                        }, function($error) {
                            return $error['response'];
                        })
                        ->wait();

                    // Convert the PSR-7 response to a RingPHP response
                    return new \GuzzleHttp\Ring\Future\CompletedFutureArray([
                        'status'         => $response->getStatusCode(),
                        'headers'        => $response->getHeaders(),
                        'body'           => $response->getBody()
                                                     ->detach(),
                        'transfer_stats' => ['total_time' => 0],
                        'effective_url'  => (string)$psr7Request->getUri(),
                    ]);
                });
            }
        }

        // Build and return the client

        return $clientBuilder->build();
    }
}


<?php namespace Cviebrock\LaravelElasticsearch;

use Elasticsearch\Client;
use Illuminate\Foundation\Application as LaravelApplication;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Laravel\Lumen\Application as LumenApplication;

/**
 * Class ServiceProvider
 *
 * @package Cviebrock\LaravelElasticsearch
 */
class ServiceProvider extends BaseServiceProvider
{

    /**
     * Bootstrap the application services.
     */
    public function boot()
    {
        $this->setUpConfig();
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $app = $this->app;

        $app->singleton('elasticsearch.factory', function($app) {
            return new Factory();
        });

        $app->singleton('elasticsearch', function($app) {
            return new Manager($app, $app['elasticsearch.factory']);
        });

        $app->alias('elasticsearch', Manager::class);

        $app->singleton(Client::class, function($app) {
            return $app['elasticsearch']->connection();
        });
    }

    protected function setUpConfig()
    {
        $source = dirname(__DIR__) . '/config/elasticsearch.php';

        if ($this->app instanceof LaravelApplication) {
            $this->publishes([$source => config_path('elasticsearch.php')], 'config');
        } elseif ($this->app instanceof LumenApplication) {
            $this->app->configure('elasticsearch');
        }

        $this->mergeConfigFrom($source, 'elasticsearch');
    }
}