Skip to content

Commit

Permalink
Add AbstractCookieDispatchTranscriptor
Browse files Browse the repository at this point in the history
It uses black magic excessively:

- wraps & replaces the EventDispatcher of the Symfony Response
- high-jacks logging-events to unwind present day cookies
  • Loading branch information
ebln committed Jun 8, 2024
1 parent ba16751 commit 85182bb
Show file tree
Hide file tree
Showing 17 changed files with 618 additions and 5 deletions.
65 changes: 65 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,71 @@
'yoda_style' => ['less_and_greater' => false],
'type_declaration_spaces' => ['elements' => ['function']],
'fully_qualified_strict_types' => false,
'single_space_around_construct' => [
'constructs_followed_by_a_single_space' => [ // default, minus const
'abstract',
'as',
'attribute',
'break',
'case',
'catch',
'class',
'clone',
'comment',
'const_import',
'continue',
'do',
'echo',
'else',
'elseif',
'enum',
'extends',
'final',
'finally',
'for',
'foreach',
'function',
'function_import',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'match',
'named_argument',
'namespace',
'new',
'open_tag_with_echo',
'php_doc',
'php_open',
'print',
'private',
'protected',
'public',
'readonly',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'try',
'type_colon',
'use',
'use_lambda',
'use_trait',
'var',
'while',
'yield',
'yield_from',
],

],
]
)
->setFinder($finder)
Expand Down
30 changes: 26 additions & 4 deletions mock/sfEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
class sfEvent
{
/** @var mixed */
protected $value = null;
/** @var string */
protected $name = '';
protected $value = null;
protected string $name = '';
protected object $subject;
protected array $parameters;

public function getName(): string
{
Expand All @@ -19,7 +20,9 @@ public function getName(): string

public function __construct($subject, string $name, array $parameters = [])
{
$this->name = $name;
$this->name = $name;
$this->subject = $subject;
$this->parameters = $parameters;
}

public function setReturnValue($value)
Expand All @@ -31,4 +34,23 @@ public function getReturnValue()
{
return $this->value;
}

public function getSubject(): object
{
return $this->subject;
}

public function offsetGet($name)
{
if (!array_key_exists($name, $this->parameters)) {
throw new \InvalidArgumentException();
}

return $this->parameters[$name];
}

public function offsetSet($name, $value)
{
$this->parameters[$name] = $value;
}
}
9 changes: 9 additions & 0 deletions mock/sfEventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ public function filter(sfEvent $event, $value): sfEvent

return $event;
}

public function notify(sfEvent $event): sfEvent
{
foreach ($this->listeners[$event->getName()] ?? [] as $listener) {
call_user_func($listener, $event);
}

return $event;
}
}
17 changes: 16 additions & 1 deletion mock/sfWebResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ public function getContent()
/**
* @return null|string
* @deprecated Only for testing! Original methods echos instead of returning
*
*/
public function sendContent()
{
Expand All @@ -248,4 +247,20 @@ public function sendContent()

return $event->getReturnValue();
}

/**
* @deprecated Only for testing! Original methods uses `header` & `setrawcookie` instead of returning
*/
public function sendHttpHeaders(): void
{
if ($this->options['logging']) {
$this->dispatcher->notify(new sfEvent($this, 'application.log', ['Send status "???"']));
}
$this->setHttpHeader('Content-Type', 'example');
foreach ($this->headers as $name => $value) {
if (null !== $this->dispatcher && $this->options['logging'] && !empty($value)) {
$this->dispatcher->notify(new sfEvent($this, 'application.log', ["Send header \"{$name}: {$value}\""]));
}
}
}
}
98 changes: 98 additions & 0 deletions src/Transcriptor/CookieDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor;

use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\CookieContainerInterface;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\DispatchSubstitutor;

/**
* Wraps sfEventDispatcher to fire PSR7 cookies in sfWebRequest
*
* There is no interface to implement and this class deliberately doesn't extend the sfEventDispatcher.
* As there is to type coercion in the Sf1 code, that should be fine.
*/
class CookieDispatcher
{
public const APPLICATION_LOG = 'application.log';
private const LOGGING_PRIORITY = null; // For debugging: overrides event's priority, @see \sfLogger

private ?\sfEventDispatcher $dispatcher;
private bool $logging;

private ?int $headerCountdown = null;

public function __construct(?\sfEventDispatcher $dispatcher, bool $logging)
{
$this->dispatcher = $dispatcher;
$this->logging = $logging;
}

/**
* @param array{0: \sfEvent} $arguments # |array{0: \sfEvent, 1: mixed}|array{0: string}|array{0: string, 1: callable}
*
* @return mixed # void|false|\sfEvent|bool|callable[]
*/
public function __call(string $name, array $arguments)
{
$this->dispatcher ??= new \sfEventDispatcher();

return call_user_func_array([$this->dispatcher, $name], $arguments); // @phpstan-ignore argument.type
}

public function notify(\sfEvent $event): \sfEvent
{ // We are only interested in logging events from the response, and pass-through everything else.
if (self::APPLICATION_LOG !== $event->getName() || !$event->getSubject() instanceof \sfWebResponse) {
return $this->passNotify($event);
}
// Override local logging for debug purposes
if (null !== self::LOGGING_PRIORITY) { // @phpstan-ignore notIdentical.alwaysFalse
$event->offsetSet('priority', self::LOGGING_PRIORITY); // Force logging
}

if ($this->logging) {
$this->passNotify($event); // Notify is not expected to change the event; Sticking to in-order logging, over preserving possible return-event.
}
/** @var \sfWebResponse $response */
$response = $event->getSubject();

/** @var string $logMessage */
$logMessage = $event->offsetGet(0);

// There is always at least on header, as Content-Type is forced in sfWebResponseX::sendHttpHeaders
if (str_starts_with($logMessage, 'Send header "')) {
// initialize countdown with the number of header lines, after the very first header was sent out…
$this->headerCountdown ??= count($response->getHttpHeaders());
--$this->headerCountdown; // decrease right away…
if (0 === $this->headerCountdown) { // so that we'll reach 0, after the last header was sent
/** @var array{__psr7cookies: CookieContainerInterface} $options */
$options = $response->getOptions();
foreach ($options[DispatchSubstitutor::PSR_7_COOKIES]->getCookies() as $cookie) {
$cookie->apply();
if ($this->logging) {
$params = ["Send PSR7 cookie \"{$cookie->getName()}\": \"{$cookie->getValue()}\""];

if (null !== self::LOGGING_PRIORITY) { // @phpstan-ignore notIdentical.alwaysFalse
$params['priority'] = self::LOGGING_PRIORITY; // Force logging
}
$this->passNotify(
new \sfEvent($this, self::APPLICATION_LOG, $params)
);
}
}
}
}

return $event;
}

private function passNotify(\sfEvent $event): \sfEvent
{
if ($this->dispatcher) {
return $this->dispatcher->notify($event);
}

return $event;
}
}
27 changes: 27 additions & 0 deletions src/Transcriptor/Response/AbstractCookieDispatchTranscriptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response;

use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\CookieContainerInterface;
use brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch\DispatchSubstitutor;
use Psr\Http\Message\ResponseInterface;

abstract class AbstractCookieDispatchTranscriptor implements CookieTranscriptorInterface
{
private DispatchSubstitutor $substitutor;

public function __construct(DispatchSubstitutor $substitutor)
{
$this->substitutor = $substitutor;
}

final public function transcribeCookies(ResponseInterface $psrResponse, \sfWebResponse $sfWebResponse): void
{
$this->substitutor->wrapDispatcher($sfWebResponse, $this->getCookieContainer($psrResponse));
}

/** Implement this method to obtain a CookieContainer from your PSR-7 Response! */
abstract protected function getCookieContainer(ResponseInterface $psrResponse): CookieContainerInterface;
}
32 changes: 32 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/AbstractCookie.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

abstract class AbstractCookie implements CookieInterface
{
/** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'None'|'Strict'} */
protected array $options;

private string $name;
private string $value;

/** @param array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'None'|'Strict'} $options */
public function __construct(string $name, string $value, array $options)
{
$this->name = $name;
$this->value = $value;
$this->options = $options;
}

public function getName(): string
{
return $this->name;
}

public function getValue(): string
{
return $this->value;
}
}
23 changes: 23 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/CookieContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

class CookieContainer
{
/** @var CookieInterface[] */
private array $cookies;

/** @param CookieInterface[] $cookies */
public function __construct(array $cookies)
{
$this->cookies = $cookies;
}

/** @return CookieInterface[] */
public function getCookies(): array
{
return $this->cookies;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

interface CookieContainerInterface
{
/** @return CookieInterface[] */
public function getCookies(): array;
}
14 changes: 14 additions & 0 deletions src/Transcriptor/Response/CookieDispatch/CookieInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace brnc\Symfony1\Message\Transcriptor\Response\CookieDispatch;

interface CookieInterface
{
public function getName(): string;

public function getValue(): string;

public function apply(): bool;
}
Loading

0 comments on commit 85182bb

Please sign in to comment.