Skip to content

Instantly share code, notes, and snippets.

@lolychank
Last active October 18, 2019 07:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lolychank/75bf5f4e74b48660a8aed951e5716495 to your computer and use it in GitHub Desktop.
Save lolychank/75bf5f4e74b48660a8aed951e5716495 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
namespace Kafkiansky\Bot\Bus;
use Kafkiansky\Bot\Component\Event\EventDispatcher;
use Kafkiansky\Bot\Component\Event\FinishRequestEvent;
use Kafkiansky\Bot\Connection;
use Kafkiansky\Bot\Http\Request;
use Kafkiansky\Bot\Http\Response;
use Kafkiansky\Bot\NextHandler;
use Kafkiansky\Bot\Security\EventSubscriber\AccessDeniedEvent;
use Kafkiansky\Bot\Workflow;
use Psr\Container\ContainerInterface;
final class CommandBus implements BusHandlerInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var CommandCollection
*/
private $commands;
/**
* @var array
*/
private $commandsName = [];
/**
* @var Workflow
*/
private $workflow;
/**
* @var Connection
*/
private $connection;
/**
* @var Request
*/
private $request;
/**
* @var Response
*/
private $response;
/**
* @var EventDispatcher
*/
private $dispatcher;
/**
* @var array
*/
private $adminCommands;
/**
* @var array
*/
private $allCommands;
public function __construct(ContainerInterface $container, CommandCollection $commands, EventDispatcher $dispatcher)
{
$this->container = $container;
$this->commands = $commands;
$this->dispatcher = $dispatcher;
$this->workflow = $container->get(Workflow::class);
$this->connection = $container->get(Connection::class);
$this->request = $container->get(Request::class);
$this->response = $container->get(Response::class);
$this->resolveCommands($commands->getCommands());
}
/**
* @param string $command
* @return void
* @throws InvalidCommandClassException
* @throws NotFoundMethodException
* @throws \ReflectionException
*/
public function handle(string $command)
{
$commandObject = new \ReflectionClass($this->match($command));
if (!$commandObject->implementsInterface(CommandInterface::class)) {
throw new InvalidCommandClassException(sprintf('Command %s must implement interface %s',
$commandObject->getName(), CommandInterface::class
));
}
$this->dispatcher->dispatch(new AccessDeniedEvent(
$this->getAdminCommands(),
$this->getAllCommands(),
$this->request->getChatId(),
$this->request->getMessageText()
));
$commandObject->newInstanceArgs($this->getConstructorArguments($commandObject))
->execute(...$this->getExecuteMethodArguments($commandObject));
$this->dispatcher->dispatch(new FinishRequestEvent(
$command,
$this->request->getChatId(),
$this->request->getMessageText(),
$this->getCommandsName()
));
}
/**
* @param string $command
* @return string
*
* @throws \InvalidArgumentException
*/
private function match(string $command): string
{
/** @var array|Envelope[] $envelope */
foreach ($this->commands->getEnvelopes() as $envelope) {
if ($command === $envelope->getName()) {
return $envelope->getClass();
}
}
/** @var NextHandler $handler */
foreach ($this->workflow->read() as $handler) {
if ($handler->previousCommand === $this->connection->getLastCommand($this->request->getChatId())) {
return $handler->next;
}
}
throw new \InvalidArgumentException(sprintf('The command %s not found in collection %s', $command, \get_class($this->commands)));
}
/**
* @param \ReflectionClass $command
* @return array
*/
private function getConstructorArguments(\ReflectionClass $command): array
{
$arguments = [];
if ($command->hasMethod('__construct')) {
foreach ($command->getConstructor()->getParameters() as $parameter) {
if ($class = $parameter->getClass()) {
$arguments[] = $this->container->get($class->getName());
} elseif ($parameter->getClass()->newInstanceWithoutConstructor() instanceof EventDispatcher) {
$class = $parameter->getClass()->newInstanceWithoutConstructor();
$arguments[] = new $class($this->container);
} else {
$arguments[] = $this->container->get($parameter->getName());
}
}
}
return $arguments;
}
/**
* @param \ReflectionClass $command
* @return array
* @throws NotFoundMethodException
* @throws \ReflectionException
*/
private function getExecuteMethodArguments(\ReflectionClass $command): array
{
if (!$command->hasMethod('execute')) {
throw new NotFoundMethodException(sprintf('The %s must define method execute()', $command->getName()));
}
$arguments = [];
foreach ($command->getMethod('execute')->getParameters() as $parameter) {
if ($class = $parameter->getClass()) {
$arguments[] = $this->container->get($class->getName());
} else {
$arguments[] = $this->container->get($parameter->getName());
}
}
return $arguments;
}
/**
* @return array
*/
private function getCommandsName(): array
{
/** @var Envelope $envelope */
foreach ($this->commands->getEnvelopes() as $envelope) {
$this->commandsName[] = $envelope->getName();
}
return $this->commandsName;
}
/**
* @return array
*/
public function getAdminCommands(): array
{
return $this->adminCommands;
}
/**
* @return array
*/
public function getAllCommands(): array
{
return $this->allCommands;
}
private function resolveCommands(array $commands)
{
foreach ($commands as $k => $v) {
if ($v === 'admin') {
$this->adminCommands[] = $k;
} else {
$this->allCommands[] = $k;
}
}
}
}
<?php
return [
Kafkiansky\Bot\Commands\StartCommand::class => 'all',
Kafkiansky\Bot\Commands\HelpCommand::class => 'all',
Kafkiansky\Bot\Commands\ListUsersCommand::class => 'admin',
Kafkiansky\Bot\Commands\VacanciesCommand::class => 'all',
Kafkiansky\Bot\Commands\CitiesCommand::class => 'all',
Kafkiansky\Bot\Commands\TagsCommand::class => 'all',
Kafkiansky\Bot\Commands\DebugInfoCommand::class => 'admin',
Kafkiansky\Bot\Commands\SetCitiesCommand::class => 'all',
];
<?php
declare(strict_types=1);
namespace Kafkiansky\Bot\Bus;
final class CommandCollection implements \Iterator
{
/**
* @var array
*/
private $commands = [];
/**
* @var array|Envelope[]
*/
private $envelopes = [];
/**
* @var int|null
*/
private $i = 0;
public function __construct(?array $commands)
{
$this->commands = \array_keys($commands);
}
/**
* @return mixed
*/
public function current()
{
return $this->commands[$this->i];
}
/**
* @return void
*/
public function next(): void
{
$this->i++;
}
/**
* @return int|null
*/
public function key(): ?int
{
return $this->i;
}
/**
* @return bool
*/
public function valid(): bool
{
return $this->i < \count($this->commands);
}
/**
* @return void
*/
public function rewind(): void
{
$this->i = 0;
}
/**
* @return array|Envelope[]
*/
public function getEnvelopes(): array
{
while ($this->valid()) {
$this->envelopes[] = new Envelope($this->current());
$this->next();
}
$this->rewind();
return $this->envelopes;
}
/**
* @return array
*/
public function getCommands(): array
{
return $this->commands;
}
}
<?php
use Kafkiansky\Bot\DI\Container;
use Kafkiansky\Bot\Workflow;
use Symfony\Component\Dotenv\Dotenv;
use Telegram\Bot\Api;
/** @var Container $container */
if (\file_exists($file = __DIR__ . '/../.env')) {
(new Dotenv())->load($file);
}
$container->set('dbUser', getenv('DB_USER'));
$container->set('dbPassword', getenv('DB_PASSWORD'));
$container->set('dbName', getenv('DB_NAME'));
$container->set('dbCharset', 'utf8');
$container->set('dbHost', getenv('DB_HOST'));
$container->set('botToken', getenv('BOT_TOKEN'));
$container->set('workflowpath', __DIR__ . '/workflow.json');
$container->set('connection', function () use ($container) {
return sprintf('mysql:host=%s;dbname=%s;charset=%s',
$container->get('dbHost'),
$container->get('dbName'),
$container->get('dbCharset')
);
});
$container->set(PDO::class, function () use ($container) {
$pdo = new PDO($container->get('connection'), $container->get('dbUser'), $container->get('dbPassword'));
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
});
$container->set(Api::class, function () use ($container) {
return new Api($container->get('botToken'));
});
$container->set(Workflow::class, function () use ($container) {
return new Workflow($container->get('workflowpath'));
});
<?php
declare(strict_types=1);
namespace Kafkiansky\Bot\Bus;
final class Envelope
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $class;
public function __construct($command)
{
try {
$reflectionObject = new \ReflectionClass($command);
$constant = $reflectionObject->getConstant('NAME');
if (\is_null($constant) || $constant === false) {
throw new \InvalidArgumentException(sprintf('The command %s must define constant NAME', $command));
}
$this->name = $constant;
$this->class = $reflectionObject->getName();
} catch (\ReflectionException $e) {
throw new \InvalidArgumentException(sprintf('%s is not a object', $command));
}
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
*/
public function getClass(): string
{
return $this->class;
}
}
<?php
declare(strict_types=1);
use Kafkiansky\Bot\Bus\CommandBus;
use Kafkiansky\Bot\Bus\CommandCollection;
use Kafkiansky\Bot\Component\Event\EventDispatcher;
use Kafkiansky\Bot\DI\Container;
use Kafkiansky\Bot\Http\Request;
require __DIR__ . '/../vendor/autoload.php';
if(!defined('STDOUT')) define('STDOUT', fopen('php://stdout', 'wb'));
/** @var Container $container */
/** @var EventDispatcher $dispatcher */
$container = require __DIR__ . '/../config/container.php';
$dispatcher = require __DIR__ . '/../config/dispatcher.php';
$commands = require __DIR__ . '/../config/commands.php';
$bus = new CommandBus($container, new CommandCollection($commands), $dispatcher);
/** @var Request $request */
$request = $container->get(Request::class);
preg_match('/(?<message>[a-zA-Z]+)/', $request->getMessageText(), $m);
$bus->handle($m['message']);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment