Skip to content

Commit

Permalink
-
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Mar 26, 2021
1 parent d28e5af commit a1dffea
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 110 deletions.
2 changes: 1 addition & 1 deletion src/Turbo/Attribute/Broadcast.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class Broadcast
/**
* Options can be any option supported by the broadcaster.
*
* @see Broadcaster for the default options // there is some coupling with Mercure here - can't we extract some options as named (generic) arguments?
* @see Broadcaster for the default options when using Mercure
*/
public function __construct(mixed ...$options)
{
Expand Down
12 changes: 7 additions & 5 deletions src/Turbo/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\UX\Turbo\DependencyInjection;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\MercureBundle\MercureBundle;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand All @@ -34,13 +35,14 @@ public function getConfigTreeBuilder(): TreeBuilder
->arrayNode('broadcast')
->{\PHP_VERSION_ID >= 80000 ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->scalarNode('entity_namespace')
->info('Prefix to strip when looking for broadcast templates')
->defaultValue('App\\Entity\\') // we should remove the default and ship it with a recipe instead
->arrayNode('entity_template_prefixes')
->fixXmlConfig('entity_template_prefix')
->defaultValue(['App\Entity\\' => 'broadcast/'])
->scalarPrototype()->end()
->end()
->arrayNode('doctrine_orm')
->info('Enable the Doctrine ORM integration')
->{class_exists(DoctrineBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{class_exists(DoctrineBundle::class) && interface_exists(EntityManagerInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->end()
->end()
->end()
Expand All @@ -51,8 +53,8 @@ public function getConfigTreeBuilder(): TreeBuilder
->children()
->arrayNode('hubs')
->fixXmlConfig('hub')
->scalarPrototype()
->info('The name of the Mercure hubs (configured in MercureBundle) to use as transports')
->scalarPrototype()->end()
->end()
->end()
->end()
Expand Down
20 changes: 12 additions & 8 deletions src/Turbo/DependencyInjection/TurboExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\UX\Turbo\DependencyInjection;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\MercureBundle\MercureBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
Expand All @@ -24,6 +25,7 @@
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Mercure\Hub;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface;
use Symfony\UX\Turbo\Doctrine\BroadcastListener;
use Symfony\UX\Turbo\Mercure\Broadcaster;
Expand Down Expand Up @@ -94,8 +96,8 @@ private function registerBroadcast(array $config, ContainerBuilder $container, L
return;
}

if (!class_exists(DoctrineBundle::class)) {
throw new InvalidConfigurationException('You cannot use the Doctrine ORM integration as "doctrine/doctrine-bundle" is not installed. Try running "composer require symfony/orm-pack".');
if (!class_exists(DoctrineBundle::class) || !interface_exists(EntityManagerInterface::class)) {
throw new InvalidConfigurationException('You cannot use the Doctrine ORM integration as the Doctrine bundle is not installed. Try running "composer require symfony/orm-pack".');
}
}

Expand All @@ -108,12 +110,14 @@ private function registerMercureTransports(array $config, ContainerBuilder $cont
return;
}

if (!class_exists(MercureBundle::class)) {
throw new InvalidConfigurationException('You cannot use the Mercure integration as "symfony/mercure-bundle" is not installed. Try running "composer require symfony/mercure-bundle".');
}
$missingDeps = array_filter([
'symfony/mercure-bundle' => !class_exists(MercureBundle::class),
'symfony/twig-pack' => !class_exists(TwigBundle::class),
'symfony/property-access' => !interface_exists(PropertyAccessorInterface::class),
]);

if (!class_exists(TwigBundle::class)) {
throw new InvalidConfigurationException('You cannot use the Mercure integration as "symfony/twig-bundle" is not installed. Try running "composer require symfony/twig-pack".');
if ($missingDeps) {
throw new InvalidConfigurationException(sprintf('You cannot use the Mercure integration as some required dependencies are missing. Try running "composer require %s".', implode(' ', $missingDeps)));
}

$loader->load('mercure.php');
Expand Down Expand Up @@ -146,7 +150,7 @@ private function registerMercureTransport(ContainerBuilder $container, array $co
$broadcaster = $container->setDefinition("turbo.mercure.{$name}.broadcaster", new ChildDefinition(Broadcaster::class));
$broadcaster->replaceArgument(0, $name);
$broadcaster->replaceArgument(2, new Reference($hubId));
$broadcaster->replaceArgument(5, $config['broadcast']['entity_namespace']);
$broadcaster->replaceArgument(4, $config['broadcast']['entity_template_prefixes']);
$broadcaster->addTag('turbo.broadcaster');
}
}
15 changes: 10 additions & 5 deletions src/Turbo/Doctrine/BroadcastListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,24 @@
final class BroadcastListener implements ResetInterface
{
private $broadcaster;

/**
* @var array<class-string, \ReflectionAttribute[]>
*/
private $broadcastedClasses;

/**
* @var \SplObjectStorage<object, object>
*/
private \SplObjectStorage $createdEntities;
private $createdEntities;
/**
* @var \SplObjectStorage<object, object>
*/
private \SplObjectStorage $updatedEntities;
private $updatedEntities;
/**
* @var \SplObjectStorage<object, object>
*/
private \SplObjectStorage $removedEntities;
private $removedEntities;

public function __construct(BroadcasterInterface $broadcaster)
{
Expand Down Expand Up @@ -109,8 +113,9 @@ private function storeEntitiesToPublish(object $entity, string $property): void
{
$class = \get_class($entity);

if ($this->broadcastedClasses[$class] ?? $this->broadcastedClasses[$class] = (bool) (new \ReflectionClass($class))->getAttributes(Broadcast::class)) {
// what happens if we don't clone removed entities here?
$this->broadcastedClasses[$class] ?? $this->broadcastedClasses[$class] = (new \ReflectionClass($class))->getAttributes(Broadcast::class);

if ($this->broadcastedClasses[$class]) {
$this->{$property}->attach('removedEntities' === $property ? clone $entity : $entity);
}
}
Expand Down
46 changes: 16 additions & 30 deletions src/Turbo/Mercure/Broadcaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Symfony\Component\Mercure\Update;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
use Symfony\UX\Turbo\Attribute\Broadcast;
use Symfony\UX\Turbo\Broadcaster\BroadcasterInterface;
use Twig\Environment;
Expand All @@ -25,17 +24,13 @@
*
* Supported options are:
*
* * topics (string[]) Mercure topics to use, defaults to an array containing the Fully Qualified Class Name with "\" characters replaced by ":" characters.
* * createTemplate (string) The Twig template to render when a new object is created
* * updateTemplate (string) The Twig template to render when a new object is updated
* * removeTemplate (string) The Twig template to render when a new object is removed
* * transports (string[]) The name of the transports to broadcast to
* * topics (string[]) The topics to use; the default topic is derived from the FQCN of the entity and from its id
* * template (string) The Twig template to render when a new object is created, updated or removed
* * private (bool) Marks Mercure updates as private
* * id (string) ID field of the SSE
* * type (string) type field of the SSE
* * retry (int) retry field of the SSE
* * transports (string[])
*
* The options can also be generated using the ExpressionLanguage language: if the option is a string, it is evaluated as an expression that must return an array.
*
* @author Kévin Dunglas <[email protected]>
*
Expand All @@ -52,8 +47,7 @@ final class Broadcaster implements BroadcasterInterface
private $twig;
private $hub;
private $propertyAccessor;
private $expressionLanguage;
private $entityNamespace;
private $templatePrefixes;

private const OPTIONS = [
// Generic options
Expand All @@ -68,7 +62,10 @@ final class Broadcaster implements BroadcasterInterface
'retry',
];

public function __construct(string $name, Environment $twig, HubInterface $hub, ?PropertyAccessorInterface $propertyAccessor, ExpressionLanguage $expressionLanguage = null, string $entityNamespace = null)
/**
* @param array<string, string> $templatePrefixes
*/
public function __construct(string $name, Environment $twig, HubInterface $hub, ?PropertyAccessorInterface $propertyAccessor, array $templatePrefixes = [])
{
if (80000 > \PHP_VERSION_ID) {
throw new \LogicException('The broadcast feature requires PHP 8.0 or greater, you must either upgrade to PHP 8 or disable it.');
Expand All @@ -78,13 +75,7 @@ public function __construct(string $name, Environment $twig, HubInterface $hub,
$this->twig = $twig;
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
$this->hub = $hub;
$this->entityNamespace = $entityNamespace;

if ($expressionLanguage) {
$this->expressionLanguage = $expressionLanguage;
} elseif (class_exists(ExpressionLanguage::class)) {
$this->expressionLanguage = new ExpressionLanguage();
}
$this->templatePrefixes = $templatePrefixes;
}

public function broadcast(object $entity, string $action): void
Expand Down Expand Up @@ -125,14 +116,6 @@ public function broadcast(object $entity, string $action): void
*/
private function normalizeOptions(object $entity, string $action, array $options): array
{
if ([0] === array_keys($options) && \is_string($options[0])) {
if (null === $this->expressionLanguage) {
throw new \RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".');
}

$options = $this->expressionLanguage->evaluate($options[0], ['entity' => $entity, 'action' => $action]);
}

if (isset($options['transports'])) {
$options['transports'] = (array) $options['transports'];
}
Expand All @@ -148,12 +131,15 @@ private function normalizeOptions(object $entity, string $action, array $options
return $options;
}

$dir = $entityClass;
if ($this->entityNamespace && 0 === strpos($entityClass, $this->entityNamespace)) {
$dir = substr($entityClass, \strlen($this->entityNamespace));
$file = $entityClass;
foreach ($this->templatePrefixes as $namespace => $prefix) {
if (0 === strpos($entityClass, $namespace)) {
$file = substr_replace($entityClass, $prefix, 0, \strlen($namespace));
break;
}
}

$options['template'] = sprintf('broadcast/%s.stream.html.twig', str_replace('\\', '/', $dir));
$options['template'] = str_replace('\\', '/', $file).'.stream.html.twig';

return $options;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/Mercure/TurboStreamListenRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function renderTurboStreamListen(Environment $env, $topic): string
{
if (\is_object($topic)) {
$topic = sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode(\get_class($topic)), rawurlencode($this->propertyAccessor->getValue($topic, 'id')));
} elseif (!preg_match('[^a-zA-Z0-9_\x7f-\xff\\\\]') && class_exists($topic)) {
} elseif (!preg_match('/[^a-zA-Z0-9_\x7f-\xff\\\\]/', $topic) && class_exists($topic)) {
// Generate a URI template to subscribe to updates for all objects of this class
$topic = sprintf(Broadcaster::TOPIC_PATTERN, rawurlencode($topic), '{id}');
}
Expand Down
Loading

0 comments on commit a1dffea

Please sign in to comment.