Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ extensions:
```neon
guzzle:
debug: %debugMode%
tracy: %debugMode%
preset: default
app: MyApp/1.0
logger:
level: info
formatter: "[{method}] {uri} {code}"
# logger: @Psr\Log\LoggerInterface
client: # config for GuzzleHttp\Client
timeout: 30
```
Expand All @@ -36,6 +43,7 @@ Everything else is in Guzzle documentation.
```php

use Contributte\Guzzlette\ClientFactory;
use Contributte\Guzzlette\GuzzleBuilder;
use GuzzleHttp\Client;
use Nette\Application\UI\Presenter;

Expand All @@ -57,5 +65,14 @@ class ExamplePresenter extends Presenter {
]);
}

public function injectGuzzleBuilder(ClientFactory $factory): void
{
$this->guzzle = $factory
->create()
->withBaseUri('https://api.example.com')
->withHttpAuth('john', 'doe')
->build();
}

}
```
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"guzzlehttp/guzzle": "^7.5.0",
"nette/di": "^3.1.2",
"nette/utils": "^3.2.8 || ^4.0.0",
"psr/log": "^3.0",
"tracy/tracy": "^2.9.5"
},
"require-dev": {
Expand Down
147 changes: 132 additions & 15 deletions src/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,166 @@

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\MessageFormatterInterface;
use GuzzleHttp\Middleware;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

class ClientFactory
{

public const FORCE_REQUEST_COLLECTION = true;

/** @var array<string, callable> */
private array $stackFns = [];

/** @var array<string, mixed> */
private array $options = [];

private SnapshotStack $snapshotStack;

private bool $debug;

private ?LoggerInterface $logger = null;

private ?MessageFormatterInterface $formatter = null;

private ?HandlerStack $handlerStack = null;

public function __construct(SnapshotStack $snapshotStack, bool $debug = false)
{
$this->snapshotStack = $snapshotStack;
$this->debug = $debug;
}

public function setFormatter(MessageFormatterInterface $formatter): void
{
$this->formatter = $formatter;
}

public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}

public function setHandlerStack(HandlerStack $handlerStack): void
{
$this->handlerStack = $handlerStack;
}

public function withDefaults(): self
{
return $this->withTracy()->withLog();
}

public function withTracy(): self
{
if ($this->debug) {
$this->with(new GuzzleHandler($this->snapshotStack), 'tracy');
}

return $this;
}

/**
* @param mixed[] $config
* @phpstan-param LogLevel::* $level
*/
public function createClient(array $config = []): Client
public function withLog(?LoggerInterface $logger = null, ?MessageFormatterInterface $formatter = null, string $level = LogLevel::DEBUG): self
{
if ($this->debug) {
$handlerStack = $config['handler'] ?? null;
if (!($handlerStack instanceof HandlerStack)) {
$handlerStack = null;
}
if ($this->logger !== null || $logger !== null) {
$resolvedLogger = $logger ?? $this->logger;
assert($resolvedLogger !== null);

$config['handler'] = $this->createHandlerStack($handlerStack);
return $this->with(
Middleware::log(
$resolvedLogger,
$formatter ?? $this->formatter ?? new MessageFormatter(),
$level,
),
'logger',
);
}

return new Client($config);
return $this;
}

public function with(callable $middleware, string $name): self
{
$this->stackFns[$name] = $middleware;

return $this;
}

public function withConfigOption(string $name, mixed $value): self
{
$this->options[$name] = $value;

return $this;
}

/**
* @param array<string, string> $headers
*/
public function withHeaders(array $headers): self
{
$this->options['headers'] = array_merge(
$this->options['headers'] ?? [], // @phpstan-ignore-line
$headers,
);

return $this;
}

public function withUserAgent(string $agent): self
{
return $this->withHeaders(['User-Agent' => $agent]);
}

public function withBaseUri(string $url): self
{
return $this->withConfigOption('base_uri', $url);
}

public function withHttpAuth(string $user, string $password): self
{
return $this->withConfigOption('auth', [$user, $password]);
}

private function createHandlerStack(?HandlerStack $handlerStack = null): HandlerStack
public function create(?HandlerStack $handlerStack = null): GuzzleBuilder
{
if ($handlerStack === null) {
$handlerStack = HandlerStack::create();
$stack = $handlerStack ?? $this->handlerStack ?? HandlerStack::create();

foreach ($this->stackFns as $name => $fn) {
$stack->push($fn, $name);
}

if ($this->debug && !isset($this->stackFns['tracy'])) {
$stack->push(new GuzzleHandler($this->snapshotStack), 'tracy');
}

$handler = new GuzzleHandler($this->snapshotStack);
$handlerStack->push($handler);
$builder = new GuzzleBuilder($stack);

foreach ($this->options as $name => $value) {
$builder->withConfigOption($name, $value);
}

return $builder;
}

/**
* @param mixed[] $config
*/
public function createClient(array $config = []): Client
{
$handlerStack = null;

if (isset($config['handler']) && $config['handler'] instanceof HandlerStack) {
$handlerStack = $config['handler'];
unset($config['handler']);
}

return $handlerStack;
return $this->create($handlerStack)->build($config);
}

}
68 changes: 66 additions & 2 deletions src/DI/GuzzleExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
use Contributte\Guzzlette\ClientFactory;
use Contributte\Guzzlette\SnapshotStack;
use GuzzleHttp\Client;
use GuzzleHttp\MessageFormatter;
use Nette\DI\CompilerExtension;
use Nette\DI\Definitions\ServiceDefinition;
use Nette\DI\Definitions\Statement;
use Nette\PhpGenerator\ClassType;
use Nette\Schema\Expect;
use Nette\Schema\Schema;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use stdClass;

/**
Expand All @@ -19,8 +25,24 @@ class GuzzleExtension extends CompilerExtension

public function getConfigSchema(): Schema
{
$expectService = Expect::anyOf(
Expect::string()->required()->assert(static fn (mixed $input): bool => is_string($input) && (str_starts_with($input, '@') || class_exists($input) || interface_exists($input))),
Expect::type(Statement::class)->required(),
);

return Expect::structure([
'debug' => Expect::bool(false),
'preset' => Expect::anyOf(null, 'default')->default('default'),
'app' => Expect::string()->nullable()->default(null),
'logger' => Expect::structure([
'level' => Expect::string(LogLevel::INFO),
'formatter' => Expect::anyOf(
clone $expectService,
Expect::string()->required(),
)->nullable()->default(null),
'logger' => clone $expectService,
]),
'tracy' => Expect::bool(false),
'client' => Expect::array()->dynamic()->default([
'timeout' => 30,
]),
Expand All @@ -38,18 +60,60 @@ public function loadConfiguration(): void

$builder->addDefinition($this->prefix('clientFactory'))
->setType(ClientFactory::class)
->setArguments([$builder->getDefinition($this->prefix('snapshotStack')), $config->debug]);
->setArguments([$builder->getDefinition($this->prefix('snapshotStack')), $config->debug || $config->tracy]);

$factoryDef = $builder->getDefinition($this->prefix('clientFactory'));
assert($factoryDef instanceof ServiceDefinition);

if ($config->app !== null) {
$factoryDef->addSetup('withUserAgent', [$config->app]);
}

if ($config->logger->formatter !== null) {
if (is_string($config->logger->formatter)) {
$factoryDef->addSetup('setFormatter', [new Statement(MessageFormatter::class, [$config->logger->formatter])]);
} else {
$factoryDef->addSetup('setFormatter', [$config->logger->formatter]);
}
}

if ($config->logger->logger !== null) {
$factoryDef->addSetup('setLogger', [is_string($config->logger->logger) ? new Statement($config->logger->logger) : $config->logger->logger]);
}

$builder->addDefinition($this->prefix('client'))
->setType(Client::class)
->setFactory('@' . $this->prefix('clientFactory') . '::createClient', ['config' => $config->client]);
}

public function beforeCompile(): void
{
$config = $this->config;
$builder = $this->getContainerBuilder();

$factoryDef = $builder->getDefinition($this->prefix('clientFactory'));
assert($factoryDef instanceof ServiceDefinition);

if ($config->logger->logger === null) {
$loggerDef = $builder->getByType(LoggerInterface::class);

if ($loggerDef !== null) {
$factoryDef->addSetup('setLogger', [$builder->getDefinition($loggerDef)]);
} elseif ($config->preset === 'default') {
$factoryDef->addSetup('setLogger', [new Statement(NullLogger::class)]);
}
}

if ($config->preset === 'default') {
$factoryDef->addSetup('withDefaults');
}
}

public function afterCompile(ClassType $class): void
{
$config = $this->config;

if ($config->debug) {
if ($config->debug || $config->tracy) {
$initialize = $class->getMethod('initialize');
$initialize->addBody(
'$this->getService(?)->addPanel(new \Contributte\Guzzlette\Tracy\Panel($this->getService(?)));',
Expand Down
Loading