Implement custom parsers/renderers with autowiring for snip content
This commit is contained in:
parent
5cec259469
commit
943177bc08
38
migrations/Version20250422222542.php
Normal file
38
migrations/Version20250422222542.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250422222542 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip ADD parser VARCHAR(255) NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
UPDATE snip SET parser = 'generic'
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE snip DROP parser
|
||||
SQL);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ use App\Form\SnipType;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use App\Service\SnipParser\Pipeline;
|
||||
use App\Service\SnipParser\ParserFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -42,23 +42,23 @@ class SnipController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/single/{snip}', name: '_single')]
|
||||
public function single(Snip $snip, Pipeline $pl): Response
|
||||
public function single(Snip $snip, ParserFactory $pf): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
return $this->render('snip/single.html.twig', [
|
||||
'snip' => $snip,
|
||||
'content' => $pl->parse($this->contentService->getActiveText($snip)),
|
||||
'content' => $pf->getBySnip($snip)->parseView($this->contentService->getActiveText($snip)),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/raw/{snip}', name: '_raw')]
|
||||
public function raw(Snip $snip, Pipeline $pl, Request $request): Response
|
||||
public function raw(Snip $snip, ParserFactory $pf, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
|
||||
|
||||
$response = new Response(
|
||||
$pl->clean($this->contentService->getActiveText($snip)),
|
||||
$pf->getBySnip($snip)->parseRaw($this->contentService->getActiveText($snip)),
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
|
@ -30,6 +30,9 @@ class Snip
|
||||
#[ORM\ManyToOne]
|
||||
private ?SnipContent $activeVersion = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $parser = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->snipContents = new ArrayCollection();
|
||||
@ -115,4 +118,16 @@ class Snip
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParser(): ?string
|
||||
{
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
public function setParser(string $parser): static
|
||||
{
|
||||
$this->parser = $parser;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,28 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use App\Service\SnipParser\ParserFactory;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SnipType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ParserFactory $parserFactory,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
dump(iterator_to_array($this->parserFactory->getChoices()));
|
||||
$builder
|
||||
->add('name')
|
||||
->add('parser', ChoiceType::class, [
|
||||
'choice_label' => fn(string $parser) => ucfirst($parser),
|
||||
'choices' => $this->parserFactory->getChoices(),
|
||||
])
|
||||
->add('content', TextareaType::class, [
|
||||
'attr' => ['rows' => 20],
|
||||
'mapped' => false,
|
||||
|
16
src/Service/SnipParser/AbstractParser.php
Normal file
16
src/Service/SnipParser/AbstractParser.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
abstract class AbstractParser implements ParserInterface
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
public function parseRaw(string $content): string
|
||||
{
|
||||
return $content;
|
||||
}
|
||||
}
|
@ -1,22 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use App\Service\SnipParser\Stages\HtmlEscapeStage;
|
||||
use App\Service\SnipParser\Stages\IncludeReferenceStage;
|
||||
use App\Service\SnipParser\Stages\UrlReferenceStage;
|
||||
use App\Service\SnipParser\Stages\ReplaceBlocksStage;
|
||||
use App\Service\SnipParser\Stages\ReplaceStage;
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
use League\Pipeline\PipelineBuilder;
|
||||
|
||||
class Pipeline
|
||||
class GenericParser extends AbstractParser
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlReferenceStage $referenceStage,
|
||||
private readonly IncludeReferenceStage $includeStage,
|
||||
) {}
|
||||
|
||||
public function parse(string $payload): string
|
||||
public function parseView(string $content): string
|
||||
{
|
||||
$builder = new PipelineBuilder();
|
||||
$pipeline = $builder
|
||||
@ -29,15 +30,15 @@ class Pipeline
|
||||
->build()
|
||||
;
|
||||
|
||||
return $pipeline->process($payload);
|
||||
return $pipeline->process($content);
|
||||
}
|
||||
|
||||
public function clean(string $payload): string
|
||||
public function parseRaw(string $content): string
|
||||
{
|
||||
return str_replace(
|
||||
['```', '``'],
|
||||
'',
|
||||
$payload
|
||||
$content
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use League\Pipeline\StageInterface;
|
||||
|
@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use App\Repository\SnipContentRepository;
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
||||
use App\Service\SnipContent\SnipContentService;
|
||||
use App\Service\SnipParser\Pipeline;
|
||||
use League\Pipeline\StageInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@ -17,7 +16,7 @@ class IncludeReferenceStage implements StageInterface
|
||||
#[Autowire(lazy: true)] private readonly Security $security,
|
||||
#[Autowire(lazy: true)] private readonly SnipRepository $snipRepository,
|
||||
#[Autowire(lazy: true)] private readonly SnipContentRepository $snipContentRepository,
|
||||
#[Autowire(lazy: true)] private readonly Pipeline $pipeline,
|
||||
#[Autowire(lazy: true)] private readonly GenericParser $pipeline,
|
||||
#[Autowire(lazy: true)] private readonly SnipContentService $snipContentService,
|
||||
) {}
|
||||
|
||||
@ -53,7 +52,7 @@ class IncludeReferenceStage implements StageInterface
|
||||
return sprintf('<span title="access denied">%s</span>', $matches[0]);
|
||||
}
|
||||
|
||||
return $this->pipeline->parse(
|
||||
return $this->pipeline->parseView(
|
||||
$this->snipContentService->rebuildText($content)
|
||||
);
|
||||
}, $payload);
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use League\Pipeline\StageInterface;
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use League\Pipeline\StageInterface;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Stages;
|
||||
namespace App\Service\SnipParser\Generic;
|
||||
|
||||
use App\Repository\SnipRepository;
|
||||
use App\Security\Voter\SnipVoter;
|
52
src/Service/SnipParser/ParserFactory.php
Normal file
52
src/Service/SnipParser/ParserFactory.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
use App\Entity\Snip;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
readonly class ParserFactory
|
||||
{
|
||||
public function __construct(
|
||||
#[AutowireLocator(ParserInterface::class, defaultIndexMethod: 'getName')]
|
||||
private ServiceLocator $locator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @template T of ParserInterface
|
||||
*
|
||||
* @param class-string<T> $id
|
||||
*
|
||||
* @return T
|
||||
* @throws ServiceNotFoundException
|
||||
*/
|
||||
public function get(string $id): ParserInterface
|
||||
{
|
||||
return $this->locator->get($id);
|
||||
}
|
||||
|
||||
public function getBySnip(Snip $snip): ParserInterface
|
||||
{
|
||||
$parser = $snip->getParser();
|
||||
if (null === $parser) {
|
||||
throw new ServiceNotFoundException(sprintf('Unknown parser for snip "%s"', $snip->getParser()));
|
||||
}
|
||||
|
||||
return $this->get($parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, ParserInterface>
|
||||
*/
|
||||
public function getAll(): iterable
|
||||
{
|
||||
return $this->locator->getIterator();
|
||||
}
|
||||
|
||||
public function getChoices(): iterable
|
||||
{
|
||||
foreach ($this->getAll() as $parser) yield $parser::getName();
|
||||
}
|
||||
}
|
15
src/Service/SnipParser/ParserInterface.php
Normal file
15
src/Service/SnipParser/ParserInterface.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag]
|
||||
interface ParserInterface
|
||||
{
|
||||
public function parseView(string $content): string;
|
||||
|
||||
public function parseRaw(string $content): string;
|
||||
|
||||
public static function getName(): string;
|
||||
}
|
18
src/Service/SnipParser/Website/HtmlParser.php
Normal file
18
src/Service/SnipParser/Website/HtmlParser.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\SnipParser\Website;
|
||||
|
||||
use App\Service\SnipParser\AbstractParser;
|
||||
|
||||
class HtmlParser extends AbstractParser
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'html';
|
||||
}
|
||||
|
||||
public function parseView(string $content): string
|
||||
{
|
||||
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($content));
|
||||
}
|
||||
}
|
@ -35,6 +35,9 @@
|
||||
<a class="nav-link" href="{{ path('logout') }}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('login') }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path('register') }}">Register</a>
|
||||
</li>
|
||||
|
Loading…
x
Reference in New Issue
Block a user