File: /var/www/dvpis2025/dvpis.kaunokolegija.lt/vendor/symfony/maker-bundle/src/Maker/MakeWebhook.php
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Maker;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\InstallDependencyTrait;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Exception\JsonException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\AttributesRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\IpsRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\PortRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\SchemeRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Yaml\Yaml;
/**
* @author Maelan LE BORGNE <maelan.leborgne@gmail.com>
*
* @internal
*/
final class MakeWebhook extends AbstractMaker implements InputAwareMakerInterface
{
use InstallDependencyTrait;
public const WEBHOOK_NAME_PATTERN = '/^[a-zA-Z_.\-\x80-\xff][a-zA-Z0-9_.\-\x80-\xff]*$/u';
private const WEBHOOK_CONFIG_PATH = 'config/packages/webhook.yaml';
private ConsoleStyle $io;
private YamlSourceManipulator $ysm;
private string $name;
/** @var array<class-string> */
private array $requestMatchers = [];
public function __construct(
private FileManager $fileManager,
private Generator $generator,
) {
}
public static function getCommandName(): string
{
return 'make:webhook';
}
public static function getCommandDescription(): string
{
return 'Create a new Webhook';
}
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
$command
->addArgument('name', InputArgument::OPTIONAL, 'Name of the webhook to create (e.g. <fg=yellow>github, stripe, ...</>)')
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeWebhook.txt'))
;
$inputConfig->setArgumentAsNonInteractive('name');
}
public function configureDependencies(DependencyBuilder $dependencies, ?InputInterface $input = null): void
{
$dependencies->addClassDependency(
Yaml::class,
'yaml'
);
}
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$this->io = $io;
$this->installDependencyIfNeeded($io, AbstractRequestParser::class, 'symfony/webhook');
if ($this->name = $input->getArgument('name') ?? '') {
if (!$this->verifyWebhookName($this->name)) {
throw new RuntimeCommandException('A webhook name can only have alphanumeric characters, underscores, dots, and dashes.');
}
return;
}
$argument = $command->getDefinition()->getArgument('name');
$question = new Question($argument->getDescription());
$question->setValidator(Validator::notBlank(...));
$this->name = $this->io->askQuestion($question);
while (!$this->verifyWebhookName($this->name)) {
$this->io->error('A webhook name can only have alphanumeric characters, underscores, dots, and dashes.');
$this->name = $this->io->askQuestion($question);
}
while (true) {
$newRequestMatcher = $this->askForNextRequestMatcher(isFirstMatcher: empty($this->requestMatchers));
if (null === $newRequestMatcher) {
break;
}
$this->requestMatchers[] = $newRequestMatcher;
}
if (\in_array(ExpressionRequestMatcher::class, $this->requestMatchers, true)) {
$this->installDependencyIfNeeded($this->io, Expression::class, 'symfony/expression-language');
}
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$requestParserDetails = $this->generator->createClassNameDetails(
Str::asClassName($this->name.'RequestParser'),
'Webhook\\'
);
$remoteEventConsumerDetails = $this->generator->createClassNameDetails(
Str::asClassName($this->name.'WebhookConsumer'),
'RemoteEvent\\'
);
$this->addToYamlConfig($this->name, $requestParserDetails);
$this->generateRequestParser(requestParserDetails: $requestParserDetails);
$this->generator->generateClass(
$remoteEventConsumerDetails->getFullName(),
'webhook/WebhookConsumer.tpl.php',
[
'webhook_name' => $this->name,
]
);
$this->generator->writeChanges();
$this->fileManager->dumpFile(self::WEBHOOK_CONFIG_PATH, $this->ysm->getContents());
$this->writeSuccessMessage($io);
}
private function verifyWebhookName(string $entityName): bool
{
return preg_match(self::WEBHOOK_NAME_PATTERN, $entityName);
}
private function addToYamlConfig(string $webhookName, ClassNameDetails $requestParserDetails): void
{
$yamlConfig = Yaml::dump(['framework' => ['webhook' => ['routing' => []]]], 4, 2);
if ($this->fileManager->fileExists(self::WEBHOOK_CONFIG_PATH)) {
$yamlConfig = $this->fileManager->getFileContents(self::WEBHOOK_CONFIG_PATH);
}
$this->ysm = new YamlSourceManipulator($yamlConfig);
$arrayConfig = $this->ysm->getData();
if (\array_key_exists($webhookName, $arrayConfig['framework']['webhook']['routing'] ?? [])) {
throw new \InvalidArgumentException('A webhook with this name already exists');
}
$arrayConfig['framework']['webhook']['routing'][$webhookName] = [
'service' => $requestParserDetails->getFullName(),
'secret' => 'your_secret_here',
];
$this->ysm->setData(
$arrayConfig
);
}
/**
* @throws \Exception
*/
private function generateRequestParser(ClassNameDetails $requestParserDetails): void
{
$useStatements = new UseStatementGenerator([
JsonException::class,
Request::class,
Response::class,
RemoteEvent::class,
AbstractRequestParser::class,
RejectWebhookException::class,
RequestMatcherInterface::class,
]);
// Use a ChainRequestMatcher if multiple matchers have been added OR if none (will be printed with an empty array)
$useChainRequestsMatcher = false;
if (1 !== \count($this->requestMatchers)) {
$useChainRequestsMatcher = true;
$useStatements->addUseStatement(ChainRequestMatcher::class);
}
$requestMatcherArguments = [];
foreach ($this->requestMatchers as $requestMatcherClass) {
$useStatements->addUseStatement($requestMatcherClass);
$requestMatcherArguments[$requestMatcherClass] = $this->getRequestMatcherArguments(requestMatcherClass: $requestMatcherClass);
if (ExpressionRequestMatcher::class === $requestMatcherClass) {
$useStatements->addUseStatement(Expression::class);
$useStatements->addUseStatement(ExpressionLanguage::class);
}
}
$this->generator->generateClass(
$requestParserDetails->getFullName(),
'webhook/RequestParser.tpl.php',
[
'use_statements' => $useStatements,
'use_chained_requests_matcher' => $useChainRequestsMatcher,
'request_matchers' => $this->requestMatchers,
'request_matcher_arguments' => $requestMatcherArguments,
]
);
}
private function askForNextRequestMatcher(bool $isFirstMatcher): ?string
{
$this->io->newLine();
$availableMatchers = $this->getAvailableRequestMatchers();
$matcherName = null;
while (null === $matcherName) {
if ($isFirstMatcher) {
$questionText = 'Add a RequestMatcher (press <return> to skip this step)';
} else {
$questionText = 'Add another RequestMatcher? Enter the RequestMatcher name (or press <return> to stop adding matchers)';
}
$choices = array_diff($availableMatchers, $this->requestMatchers);
$question = new ChoiceQuestion($questionText, array_values(['<skip>'] + $choices), 0);
$matcherName = $this->io->askQuestion($question);
if ('<skip>' === $matcherName) {
return null;
}
}
return $matcherName;
}
/** @return string[] */
private function getAvailableRequestMatchers(): array
{
return [
AttributesRequestMatcher::class,
ExpressionRequestMatcher::class,
HostRequestMatcher::class,
IpsRequestMatcher::class,
IsJsonRequestMatcher::class,
MethodRequestMatcher::class,
PathRequestMatcher::class,
PortRequestMatcher::class,
SchemeRequestMatcher::class,
];
}
private function getRequestMatcherArguments(string $requestMatcherClass): string
{
return match ($requestMatcherClass) {
AttributesRequestMatcher::class => '[\'attributeName\' => \'regex\']',
ExpressionRequestMatcher::class => 'new ExpressionLanguage(), new Expression(\'expression\')',
HostRequestMatcher::class, PathRequestMatcher::class => '\'regex\'',
IpsRequestMatcher::class => '[\'127.0.0.1\']',
IsJsonRequestMatcher::class => '',
MethodRequestMatcher::class => '\'POST\'',
PortRequestMatcher::class => '443',
SchemeRequestMatcher::class => '\'https\'',
default => '[]',
};
}
}