diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 018670d..1b43546 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest'] - php: ['7.4'] + php: ['8.2'] name: PHP ${{ matrix.php }} - ${{ matrix.os }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 24f7bd5..2421cb7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: ['ubuntu-latest', 'windows-latest'] - php: ['7.2', '7.3', '7.4', '8.0', '8.1'] + php: ['8.1', '8.2', '8.3'] name: PHP ${{ matrix.php }} - ${{ matrix.os }} diff --git a/README.md b/README.md index aa33e8f..4c8eaa3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ In the root directory of your Symfony project, open a terminal and enter. ```shell composer require pug-php/pug-symfony ``` -When your are asked to install automatically needed settings, enter yes. +When you are asked to install automatically needed settings, enter yes. It for any reason, you do not can or want to use it, you will have to add to your **config/bundles.php** file: @@ -26,29 +26,50 @@ Pug\PugSymfonyBundle\PugSymfonyBundle::class => ['all' => true], ## Usage Create Pug views by creating files with .pug extension -in **app/Resources/views** such as contact.pug: +in **templates** such as contact.pug: ```pug h1 | Contact =name ``` -Note: standard Twig functions are also available in your pug templates, for instance: -```pug -!=form_start(form, {method: 'GET'}) +Then inject `Pug\PugSymfonyEngine` to call it in your controller: +```php +namespace App\Controller; + +use Pug\PugSymfonyEngine; +use Symfony\Component\HttpKernel\Attribute\AsController; +use Symfony\Component\Routing\Annotation\Route; + +#[AsController] +class MyController +{ + #[Route('/contact')] + public function contactAction(PugSymfonyEngine $pug) + { + return $pug->renderResponse('contact/contact.pug', [ + 'name' => 'Us', + ]); + } +} ``` -Then call it in your controller: +Or alternatively you can use `\Pug\Symfony\Traits\PugRenderer` to call directly `->render()` from +any method of a controller (or service): + ```php namespace App\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Pug\Symfony\Traits\PugRenderer; +use Symfony\Component\HttpKernel\Attribute\AsController; +use Symfony\Component\Routing\Annotation\Route; -class MyController extends AbstractController +#[AsController] +class MyController { - /** - * @Route("/contact") - */ + use PugRenderer; + + #[Route('/contact')] public function contactAction() { return $this->render('contact/contact.pug', [ @@ -58,6 +79,30 @@ class MyController extends AbstractController } ``` +No matter if your controller extends `AbstractController` as it can also render twig views, so it will just +work the same as before rather you `->render('view.html.twig')` or `->render('view.pug')`. + +Note: standard Twig functions are also available in your pug templates, for instance: +```pug +!=form(form) +``` + +As per https://symfony.com/doc/current/forms.html + +Pass the FormView as usual from the controller: +```php +$task = new Task(); +// ... + +$form = $this->createFormBuilder($task) + // ... + ->getForm(); + +return $pug->renderResponse('home.pug', [ + 'form' => $form->createView(), +]); +``` + ## Configure You can inject `Pug\PugSymfonyEngine` to change options, share values, add plugins to Pug @@ -65,6 +110,7 @@ at route level: ```php // In a controller method +#[Route('/contact')] public function contactAction(\Pug\PugSymfonyEngine $pug) { $pug->setOptions(array( @@ -74,14 +120,16 @@ public function contactAction(\Pug\PugSymfonyEngine $pug) )); $pug->share('globalVar', 'foo'); $pug->getRenderer()->addKeyword('customKeyword', $bar); - - return $this->render('contact/contact.pug', [ + + return $pug->renderResponse('contact/contact.pug', [ 'name' => 'Us', ]); } ``` -Same can be ran globally on a given event such as `onKernelView` to apply customization before any +If you use the `PugRenderer` trait, you don't need to inject the service again and can just use `$this->pug`. + +Same can be run globally on a given event such as `onKernelView` to apply customization before any view rendering. See the options in the pug-php documentation: https://phug-lang.com/#options @@ -108,7 +156,7 @@ twig: ``` -Make the translator available in every views: +Make the translator available in every view: ```pug p=translator.trans('Hello %name%', {'%name%': 'Jack'}) ``` diff --git a/composer.json b/composer.json index c2cccba..ccbc510 100644 --- a/composer.json +++ b/composer.json @@ -6,22 +6,23 @@ "description": "Pug template engine for Symfony", "type": "library", "require": { - "php": "^7.2.5 || ^8.0", - "phug/component": "^1.1.0", - "pug/installer": "^1.0.0", - "pug-php/pug": "^3.4.0", - "pug-php/pug-assets": "^1.0.1", - "symfony/framework-bundle": "^5.0", - "symfony/http-foundation": "^5.0", - "symfony/http-kernel": "^5.0", - "symfony/security-bundle": "^5.0", - "symfony/templating": "^5.0", - "symfony/twig-bridge": "^5.0", - "twig/twig": "^3.0.0" + "php": ">=8.1", + "phug/component": "^1.1.4", + "pug/installer": "^1.0.1", + "pug-php/pug": "^3.5.0", + "pug-php/pug-assets": "^1.1.4", + "symfony/framework-bundle": "^6.0", + "symfony/http-foundation": "^6.0", + "symfony/http-kernel": "^6.0", + "symfony/security-bundle": "^6.0", + "symfony/templating": "^6.0", + "symfony/twig-bridge": "^6.0", + "twig/twig": "^3.5.0" }, "require-dev": { "phpunit/phpunit": "^8.5", - "symfony/symfony": "^5.0" + "symfony/symfony": "^6.0", + "monolog/monolog": "^3.2" }, "minimum-stability": "stable", "license": "MIT", diff --git a/src/Pug/Exceptions/ReservedVariable.php b/src/Pug/Exceptions/ReservedVariable.php index 24bf360..df78dab 100644 --- a/src/Pug/Exceptions/ReservedVariable.php +++ b/src/Pug/Exceptions/ReservedVariable.php @@ -1,5 +1,7 @@ pugSymfonyEngine = $pugSymfonyEngine; - parent::__construct(null); - } - - protected function configure() + public function __construct(protected readonly PugSymfonyEngine $pugSymfonyEngine) { - $this - ->setName('assets:publish') - ->setDescription('Export your assets in the web directory.'); + parent::__construct(); } - protected function cacheTemplates(Renderer $pug) + protected function cacheTemplates(Renderer $pug): array { $success = 0; $errors = 0; diff --git a/src/Pug/PugSymfonyBundle/PugExtension.php b/src/Pug/PugSymfonyBundle/PugExtension.php new file mode 100644 index 0000000..3ded5b7 --- /dev/null +++ b/src/Pug/PugSymfonyBundle/PugExtension.php @@ -0,0 +1,25 @@ + + * @author Jeremy Mikola + */ +class PugExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/config')); + $loader->load('pug.php'); + } +} diff --git a/src/Pug/PugSymfonyBundle/PugSymfonyBundle.php b/src/Pug/PugSymfonyBundle/PugSymfonyBundle.php index 777fea1..768ff65 100644 --- a/src/Pug/PugSymfonyBundle/PugSymfonyBundle.php +++ b/src/Pug/PugSymfonyBundle/PugSymfonyBundle.php @@ -1,54 +1,30 @@ container = $container; - - if ($container) { - /** @var KernelInterface $kernel */ - $kernel = $container->get('kernel'); - $engine = new PugSymfonyEngine($kernel); - /** @var ReflectionProperty $propertyAccessor */ - $services = static::getPrivateProperty($container, 'services', $propertyAccessor); - $services[PugSymfonyEngine::class] = $engine; - $propertyAccessor->setValue($container, $services); - } + $extension = new PugExtension(); + $containerBuilder->registerExtension($extension); + $containerBuilder->loadFromExtension($extension->getAlias()); } public function registerCommands(Application $application) { - $method = new ReflectionMethod(AssetsPublishCommand::class, '__construct'); - $class = $method->getNumberOfParameters() === 1 ? $method->getParameters()[0]->getClass() : null; - - if ($class && $class->getName() === PugSymfonyEngine::class) { - /** @var PugSymfonyEngine $engine */ - $engine = $this->container->get(PugSymfonyEngine::class); - - $application->addCommands([ - new AssetsPublishCommand($engine), - ]); - } + $application->addCommands([ + $this->container->get(AssetsPublishCommand::class), + ]); } } diff --git a/src/Pug/PugSymfonyBundle/config/pug.php b/src/Pug/PugSymfonyBundle/config/pug.php new file mode 100644 index 0000000..697c9dc --- /dev/null +++ b/src/Pug/PugSymfonyBundle/config/pug.php @@ -0,0 +1,23 @@ +services() + ->defaults() + ->autowire() + ->autoconfigure(); + + $services->load('Pug\\', __DIR__.'/../../*') + ->exclude([ + __DIR__.'/../../Exceptions', + __DIR__.'/../../PugSymfonyBundle', + __DIR__.'/../../Symfony', + __DIR__.'/../../Twig', + ]); + + $services->load('Pug\\PugSymfonyBundle\\Command\\', __DIR__.'/../../PugSymfonyBundle/Command/*') + ->public(); +}; diff --git a/src/Pug/PugSymfonyEngine.php b/src/Pug/PugSymfonyEngine.php index 71f78cd..d174439 100644 --- a/src/Pug/PugSymfonyEngine.php +++ b/src/Pug/PugSymfonyEngine.php @@ -1,5 +1,7 @@ getContainer(); - $this->kernel = $kernel; $this->container = $container; $this->userOptions = ($this->container->hasParameter('pug') ? $this->container->getParameter('pug') : null) ?: []; - $this->enhanceTwig(); + $this->enhanceTwig($twig); $this->onNode([$this, 'handleTwigInclude']); } @@ -98,7 +109,7 @@ protected function crawlDirectories(string $srcDir, array &$assetsDirectories, a protected function getFileFromName(string $name, string $directory = null): string { - $parts = explode(':', strval($name)); + $parts = explode(':', $name); if (count($parts) > 1) { $name = $parts[2]; @@ -166,7 +177,7 @@ public function getParameters(array $locals = []): array $locals = array_merge( $this->getOptionDefault('globals', []), $this->getOptionDefault('shared_variables', []), - $locals + $locals, ); foreach (['context', 'blocks', 'macros', 'this'] as $forbiddenKey) { @@ -183,8 +194,8 @@ public function getParameters(array $locals = []): array /** * Render a template by name. * - * @param string|\Symfony\Component\Templating\TemplateReferenceInterface $name - * @param array $parameters + * @param string|TemplateReferenceInterface $name + * @param array $parameters * * @throws ErrorException when a forbidden parameter key is used * @throws Exception when the PHP code generated from the pug code throw an exception @@ -195,15 +206,15 @@ public function render($name, array $parameters = []): string { return $this->getRenderer()->renderFile( $this->getFileFromName($name), - $this->getParameters($parameters) + $this->getParameters($parameters), ); } /** * Render a template string. * - * @param string|\Symfony\Component\Templating\TemplateReferenceInterface $name - * @param array $locals + * @param string|TemplateReferenceInterface $name + * @param array $locals * * @throws ErrorException when a forbidden parameter key is used * @@ -211,19 +222,16 @@ public function render($name, array $parameters = []): string */ public function renderString($code, array $locals = []): string { - $pug = $this->getRenderer(); - $method = method_exists($pug, 'renderString') ? 'renderString' : 'render'; - - return $pug->$method( + return $this->getRenderer()->renderString( $code, - $this->getParameters($locals) + $this->getParameters($locals), ); } /** * Check if a template exists. * - * @param string|\Symfony\Component\Templating\TemplateReferenceInterface $name + * @param string|TemplateReferenceInterface $name * * @return bool */ @@ -241,14 +249,14 @@ public function exists($name): bool /** * Check if a file extension is supported by Pug. * - * @param string|\Symfony\Component\Templating\TemplateReferenceInterface $name + * @param string|TemplateReferenceInterface $name * * @return bool */ public function supports($name): bool { foreach ($this->getOptionDefault('extensions', ['.pug', '.jade']) as $extension) { - if (substr($name, -strlen($extension)) === $extension) { + if ($extension && str_ends_with($name, $extension)) { return true; } } @@ -266,9 +274,10 @@ public function getRenderArguments(string $name, array $locals): array $dispatcher = $container->get('event_dispatcher'); $dispatcher->dispatch($event, RenderEvent::NAME); - $interceptors = array_map(static function (string $interceptorClass) use ($container) { - return $container->get($interceptorClass); - }, $this->userOptions['interceptors'] ?? []); + $interceptors = array_map( + static fn (string $interceptorClass) => $container->get($interceptorClass), + $this->userOptions['interceptors'] ?? [], + ); array_walk($interceptors, static function (InterceptorInterface $interceptor) use ($event) { $interceptor->intercept($event); @@ -278,10 +287,31 @@ public function getRenderArguments(string $name, array $locals): array return [$event->getName(), $this->getParameters($event->getLocals())]; } + public function renderResponse( + string|TemplateReferenceInterface $view, + array $parameters = [], + ?Response $response = null, + ): Response { + $content = $this->render($view, $parameters); + $response ??= new Response(); + + if ($response->getStatusCode() === 200) { + foreach ($parameters as $v) { + if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) { + $response->setStatusCode(422); + + break; + } + } + } + + $response->setContent($content); + + return $response; + } + protected static function extractUniquePaths(array $paths): array { - return array_unique(array_map(function ($path) { - return realpath($path) ?: $path; - }, $paths)); + return array_unique(array_map(static fn ($path) => realpath($path) ?: $path, $paths)); } } diff --git a/src/Pug/Symfony/Contracts/InstallerInterface.php b/src/Pug/Symfony/Contracts/InstallerInterface.php index 1229ce3..6455f28 100644 --- a/src/Pug/Symfony/Contracts/InstallerInterface.php +++ b/src/Pug/Symfony/Contracts/InstallerInterface.php @@ -1,5 +1,7 @@ nodeHandler = $nodeHandler; } + public function getTwig(): Environment + { + return $this->twig; + } + protected function getRendererOptions(): array { if ($this->options === null) { @@ -108,10 +93,10 @@ protected function getRendererOptions(): array } $srcDir = $projectDirectory.'/src'; + $assetsDirectories[] = $srcDir.'/Resources/assets'; $webDir = $projectDirectory.'/public'; - $baseDir = isset($this->userOptions['baseDir']) - ? $this->userOptions['baseDir'] - : $this->crawlDirectories($srcDir, $assetsDirectories, $viewDirectories); + $baseDir = $this->userOptions['baseDir'] + ?? $this->crawlDirectories($srcDir, $assetsDirectories, $viewDirectories); $baseDir = $baseDir && file_exists($baseDir) ? realpath($baseDir) : $baseDir; $this->defaultTemplateDirectory = $baseDir; @@ -138,9 +123,10 @@ protected function getRendererOptions(): array (new Filesystem())->mkdir($cache); } - $options['paths'] = array_unique(array_filter($options['viewDirectories'], function ($path) use ($baseDir) { - return $path !== $baseDir; - })); + $options['paths'] = array_unique(array_filter( + $options['viewDirectories'], + static fn ($path) => $path !== $baseDir, + )); $this->options = $options; } @@ -269,12 +255,12 @@ protected function interpolateTwigFunctions(string $code): string return $output; } - protected function getTokenImage($token): string + protected function getTokenImage(array|string $token): string { return is_array($token) ? $token[1] : $token; } - protected function pushArgument(array &$arguments, string &$argument, bool &$argumentNeedInterpolation) + protected function pushArgument(array &$arguments, string &$argument, bool &$argumentNeedInterpolation): void { $argument = trim($argument); @@ -302,25 +288,15 @@ protected function copyTwigFunction(TwigFunction $function): void $this->twigHelpers[$name] = $function; } - protected function enhanceTwig(): void + protected function enhanceTwig($twig): void { - $this->twig = $this->container->has('twig') ? $this->container->get('twig') : null; + $twig ??= $this->container->has('twig') ? $this->container->get('twig') : null; - if (!($this->twig instanceof TwigEnvironment)) { + if (!($twig instanceof TwigEnvironment)) { throw new RuntimeException('Twig needs to be configured.'); } - $this->twig = Environment::fromTwigEnvironment($this->twig, $this, $this->container); - - $services = static::getPrivateProperty($this->container, 'services', $propertyAccessor); - $key = isset($services['.container.private.twig']) ? '.container.private.twig' : 'twig'; - $services[$key] = $this->twig; - $propertyAccessor->setValue($this->container, $services); - } - - protected function getTwig(): Environment - { - return $this->twig; + $this->twig = Environment::fromTwigEnvironment($twig, $this, $this->container); } protected function copyTwigFunctions(): void @@ -339,7 +315,10 @@ protected function copyTwigFunctions(): void if (!$assetExtension) { $assetExtension = new AssetExtension(new Packages(new Package(new EmptyVersionStrategy()))); $twig->extensions[AssetExtension::class] = $assetExtension; - $twig->addExtension($assetExtension); + + if (!$twig->hasExtension(AssetExtension::class)) { + $twig->addExtension($assetExtension); + } } $helpers = [ @@ -352,7 +331,10 @@ protected function copyTwigFunctions(): void if (!isset($twig->extensions[$class])) { $twig->extensions[$class] = $helper; - $twig->addExtension($helper); + + if (!$twig->hasExtension($class)) { + $twig->addExtension($helper); + } } } @@ -367,12 +349,14 @@ protected function copyTwigFunctions(): void protected function getHttpFoundationExtension(): HttpFoundationExtension { /* @var RequestStack $stack */ - $stack = $this->container->get('request_stack'); + $stack = $this->stack ?? $this->container->get('request_stack'); /* @var RequestContext $context */ - $context = $this->container->has('router.request_context') - ? $this->container->get('router.request_context') - : $this->container->get('router')->getContext(); + $context = $this->context ?? ( + $this->container->has('router.request_context') + ? $this->container->get('router.request_context') + : $this->container->get('router')->getContext() + ); return new HttpFoundationExtension(new UrlHelper($stack, $context)); } diff --git a/src/Pug/Symfony/Traits/Installer.php b/src/Pug/Symfony/Traits/Installer.php index c24e72a..ffc086e 100644 --- a/src/Pug/Symfony/Traits/Installer.php +++ b/src/Pug/Symfony/Traits/Installer.php @@ -1,5 +1,7 @@ isInteractive() || $io->askConfirmation($message); } - protected static function installSymfonyBundle(IOInterface $io, $dir, $bundle, $bundleClass, $proceedTask, &$flags) - { + protected static function installSymfonyBundle( + IOInterface $io, + string $dir, + string $bundle, + string $bundleClass, + callable $proceedTask, + int &$flags, + ): void { $appFile = $dir.'/config/bundles.php'; $contents = @file_get_contents($appFile) ?: ''; @@ -52,7 +60,7 @@ protected static function installSymfonyBundle(IOInterface $io, $dir, $bundle, $ * * @return bool */ - protected static function installInSymfony5($event, $dir) + protected static function installInSymfony5($event, $dir): bool { $io = $event->getIO(); $baseDirectory = __DIR__.'/../../../..'; diff --git a/src/Pug/Symfony/Traits/Options.php b/src/Pug/Symfony/Traits/Options.php index ede1b38..d8bf166 100644 --- a/src/Pug/Symfony/Traits/Options.php +++ b/src/Pug/Symfony/Traits/Options.php @@ -1,5 +1,7 @@ getRenderer(); - return method_exists($pug, 'hasOption') && !$pug->hasOption($name) - ? $default - : $pug->getOption($name); + return $pug->hasOption($name) ? $pug->getOption($name) : $default; } /** diff --git a/src/Pug/Symfony/Traits/PrivatePropertyAccessor.php b/src/Pug/Symfony/Traits/PrivatePropertyAccessor.php index 4971566..c3ed4ce 100644 --- a/src/Pug/Symfony/Traits/PrivatePropertyAccessor.php +++ b/src/Pug/Symfony/Traits/PrivatePropertyAccessor.php @@ -1,5 +1,7 @@ setAccessible(true); + try { + $propertyAccessor = new ReflectionProperty($object, $property); - return $propertyAccessor->getValue($object); + return $propertyAccessor->getValue($object); + } catch (ReflectionException) { + return null; + } } } diff --git a/src/Pug/Symfony/Traits/PugRenderer.php b/src/Pug/Symfony/Traits/PugRenderer.php new file mode 100644 index 0000000..0d1ea7d --- /dev/null +++ b/src/Pug/Symfony/Traits/PugRenderer.php @@ -0,0 +1,29 @@ +pug = $pug; + } + + public function render( + string|TemplateReferenceInterface $view, + array $parameters = [], + ?Response $response = null, + ): Response { + return $this->pug->renderResponse($view, $parameters, $response); + } +} diff --git a/src/Pug/Twig/Environment.php b/src/Pug/Twig/Environment.php index fdbb08e..90a78b3 100644 --- a/src/Pug/Twig/Environment.php +++ b/src/Pug/Twig/Environment.php @@ -1,11 +1,13 @@ rootEnv) { + if (!($this->rootEnv ?? null)) { throw $error; } try { return $this->rootEnv->getRuntime($class); - } catch (RuntimeError $_) { + } catch (RuntimeError) { throw $error; } } } - public static function fromTwigEnvironment(TwigEnvironment $baseTwig, PugSymfonyEngine $pugSymfonyEngine, ContainerInterface $container) - { + public static function fromTwigEnvironment( + TwigEnvironment $baseTwig, + PugSymfonyEngine $pugSymfonyEngine, + ContainerInterface $container, + ): static { $twig = new static($baseTwig->getLoader(), [ 'debug' => $baseTwig->isDebug(), 'charset' => $baseTwig->getCharset(), @@ -118,17 +113,11 @@ public static function fromTwigEnvironment(TwigEnvironment $baseTwig, PugSymfony return $twig; } - /** - * @param PugSymfonyEngine $pugSymfonyEngine - */ - public function setPugSymfonyEngine(PugSymfonyEngine $pugSymfonyEngine) + public function setPugSymfonyEngine(PugSymfonyEngine $pugSymfonyEngine): void { $this->pugSymfonyEngine = $pugSymfonyEngine; } - /** - * @param ContainerInterface $container - */ public function setContainer(ContainerInterface $container): void { $this->container = $container; @@ -196,11 +185,7 @@ public function compileSource(Source $source): string public function loadTemplate(string $cls, string $name, int $index = null): Template { - if ($index !== null) { - $cls .= '___'.$index; - } - - $this->classNames[$name] = $cls; + $this->classNames[$name] = $cls.($index === null ? '' : '___'.$index); return parent::loadTemplate($cls, $name, $index); } @@ -226,7 +211,6 @@ public function getTemplateClass(string $name, int $index = null): string public function compileCode(TwigFunction $function, string $code) { $name = $function->getName(); - $arguments[] = $name; $parser = new Parser($this); $path = '__twig_function_'.$name.'_'.sha1($code).'.html.twig'; $stream = $this->tokenize(new Source($code, $path, $path)); diff --git a/tests/Pug/AbstractController.php b/tests/Pug/AbstractController.php deleted file mode 100644 index f4ed673..0000000 --- a/tests/Pug/AbstractController.php +++ /dev/null @@ -1,7 +0,0 @@ -twig)) { + $this->twig = new Environment(new FilesystemLoader( + __DIR__.'/../project-s5/templates/', + )); + $this->twig->addExtension(new CsrfExtension()); + $this->twig->addExtension(new FormExtension()); + $this->twig->addExtension(new LogoutUrlExtension(new LogoutUrlGenerator())); + $this->twig->addExtension(new TranslationExtension()); + $this->twig->addRuntimeLoader(new FactoryRuntimeLoader([ + CsrfRuntime::class => static fn () => new CsrfRuntime(new TestCsrfTokenManager()), + ])); + } + + return $this->twig; + } + + protected function getPugSymfonyEngine(?KernelInterface $kernel = null): PugSymfonyEngine + { + $twig = $this->getTwigEnvironment(); + $this->pugSymfony ??= new PugSymfonyEngine( + $kernel ?? self::$kernel, + $twig, + new RequestStack(), + new RequestContext(), + ); + $twig->setPugSymfonyEngine($this->pugSymfony); + + return $this->pugSymfony; + } private static function getConfigFiles(): array { @@ -74,22 +119,19 @@ public function setUp(): void self::bootKernel(); - $this->addFormRenderer(static::$container); + $this->addFormRenderer(); } - protected function addFormRenderer(ContainerInterface $container) + protected function addFormRenderer() { require_once __DIR__.'/TestCsrfTokenManager.php'; - /** @var Environment $twig */ - $twig = $container->get('twig'); + $twig = $this->getTwigEnvironment(); $csrfManager = new TestCsrfTokenManager(); $formEngine = new TwigRendererEngine(['form_div_layout.html.twig'], $twig); $twig->addRuntimeLoader(new FactoryRuntimeLoader([ - FormRenderer::class => static function () use ($formEngine, $csrfManager) { - return new FormRenderer($formEngine, $csrfManager); - }, + FormRenderer::class => static fn () => new FormRenderer($formEngine, $csrfManager), ])); } } diff --git a/tests/Pug/Composer/BaseIO.php b/tests/Pug/Composer/BaseIO.php index 6dba919..2a6ff3f 100644 --- a/tests/Pug/Composer/BaseIO.php +++ b/tests/Pug/Composer/BaseIO.php @@ -16,6 +16,7 @@ use Composer\Util\ProcessExecutor; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Stringable; abstract class BaseIO implements IOInterface, LoggerInterface { @@ -149,15 +150,10 @@ public function loadConfiguration(Config $config) /** * System is unusable. - * - * @param string $message - * @param array $context - * - * @return null */ - public function emergency($message, array $context = []) + public function emergency(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::EMERGENCY, $message, $context); + $this->log(LogLevel::EMERGENCY, $message, $context); } /** @@ -165,44 +161,29 @@ public function emergency($message, array $context = []) * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * - * @return null */ - public function alert($message, array $context = []) + public function alert(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::ALERT, $message, $context); + $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return null */ - public function critical($message, array $context = []) + public function critical(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::CRITICAL, $message, $context); + $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. - * - * @param string $message - * @param array $context - * - * @return null */ - public function error($message, array $context = []) + public function error(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::ERROR, $message, $context); + $this->log(LogLevel::ERROR, $message, $context); } /** @@ -210,68 +191,42 @@ public function error($message, array $context = []) * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * - * @return null */ - public function warning($message, array $context = []) + public function warning(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::WARNING, $message, $context); + $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. - * - * @param string $message - * @param array $context - * - * @return null */ - public function notice($message, array $context = []) + public function notice(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::NOTICE, $message, $context); + $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return null */ - public function info($message, array $context = []) + public function info(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::INFO, $message, $context); + $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return null */ - public function debug($message, array $context = []) + public function debug(Stringable|string $message, array $context = []): void { - return $this->log(LogLevel::DEBUG, $message, $context); + $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return null */ - public function log($level, $message, array $context = []) + public function log($level, Stringable|string $message, array $context = []): void { if (in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR])) { $this->writeError(''.$message.'', true, self::NORMAL); diff --git a/tests/Pug/PugSymfonyBundle/Command/AssetsPublishCommandTest.php b/tests/Pug/PugSymfonyBundle/Command/AssetsPublishCommandTest.php index 0b8ff09..c2317b6 100644 --- a/tests/Pug/PugSymfonyBundle/Command/AssetsPublishCommandTest.php +++ b/tests/Pug/PugSymfonyBundle/Command/AssetsPublishCommandTest.php @@ -3,7 +3,6 @@ namespace Pug\Tests\PugSymfonyBundle\Command; use Pug\PugSymfonyBundle\Command\AssetsPublishCommand; -use Pug\PugSymfonyEngine; use Pug\Tests\AbstractTestCase; use Pug\Tests\TestKernel; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -19,7 +18,7 @@ public function testCommand() self::$kernel->boot(); $application = new Application(self::$kernel); - $application->add(new AssetsPublishCommand(new PugSymfonyEngine(self::$kernel))); + $application->add(new AssetsPublishCommand($this->getPugSymfonyEngine())); // Convert PHP style files to JS style $customHelperFile = __DIR__.'/../../../project-s5/templates/custom-helper.pug'; diff --git a/tests/Pug/PugSymfonyEngineTest.php b/tests/Pug/PugSymfonyEngineTest.php index fefde50..01874e9 100644 --- a/tests/Pug/PugSymfonyEngineTest.php +++ b/tests/Pug/PugSymfonyEngineTest.php @@ -5,31 +5,45 @@ use App\Service\PugInterceptor; use DateTime; use ErrorException; -use InvalidArgumentException; +use Phug\CompilerException; +use Phug\Util\SourceLocation; use Pug\Exceptions\ReservedVariable; use Pug\Filter\AbstractFilter; use Pug\Pug; use Pug\PugSymfonyEngine; use Pug\Symfony\MixedLoader; +use Pug\Symfony\Traits\HelpersHandler; use Pug\Symfony\Traits\PrivatePropertyAccessor; use Pug\Twig\Environment; use ReflectionException; use ReflectionProperty; use RuntimeException; use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Csrf\CsrfExtension; +use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormRegistry; +use Symfony\Component\Form\ResolvedFormTypeFactory; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage as BaseTokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator as BaseLogoutUrlGenerator; +use Symfony\Component\Translation\Translator; use Twig\Error\LoaderError; use Twig\Loader\ArrayLoader; use Twig\TwigFunction; class TokenStorage extends BaseTokenStorage { - public function getToken(): string + public function getToken(): ?TokenInterface { - return 'the token'; + return new NullToken(); } } @@ -82,27 +96,40 @@ public function setDueDate(DateTime $dueDate = null) } } -if (!class_exists('Symfony\Bundle\FrameworkBundle\Controller\AbstractController')) { - include __DIR__.'/AbstractController.php'; -} - -class TestController extends AbstractController +class TestController { public function index() { - try { - return $this->createFormBuilder(new Task()) - ->add('name', 'Symfony\Component\Form\Extension\Core\Type\TextType') - ->add('dueDate', 'Symfony\Component\Form\Extension\Core\Type\DateType') - ->add('save', 'Symfony\Component\Form\Extension\Core\Type\SubmitType', ['label' => 'Foo']) - ->getForm(); - } catch (InvalidArgumentException $e) { - return $this->createFormBuilder(new Task()) - ->add('name', 'text') - ->add('dueDate', 'date') - ->add('save', 'submit', ['label' => 'Foo']) - ->getForm(); - } + $csrfGenerator = new UriSafeTokenGenerator(); + $csrfManager = new CsrfTokenManager($csrfGenerator, new class() implements TokenStorageInterface { + public function getToken(string $tokenId): string + { + return "token:$tokenId"; + } + + public function setToken(string $tokenId, #[\SensitiveParameter] string $token) + { + // noop + } + + public function removeToken(string $tokenId): ?string + { + return "token:$tokenId"; + } + + public function hasToken(string $tokenId): bool + { + return true; + } + }); + $extensions = [new CsrfExtension($csrfManager)]; + $factory = new FormFactory(new FormRegistry($extensions, new ResolvedFormTypeFactory())); + + return $factory->createBuilder(FormType::class, new Task()) + ->add('name', TextType::class) + ->add('dueDate', DateType::class) + ->add('save', SubmitType::class, ['label' => 'Foo']) + ->getForm(); } } @@ -115,15 +142,16 @@ public function testRequireTwig() self::expectException(RuntimeException::class); self::expectExceptionMessage('Twig needs to be configured.'); - $container = self::$kernel->getContainer(); - foreach (['services', 'aliases', 'fileMap', 'methodMap'] as $name) { - /** @var ReflectionProperty $propertyAccessor */ - $services = static::getPrivateProperty($container, $name, $propertyAccessor); - unset($services['twig']); - $propertyAccessor->setValue($container, $services); - } + $object = new class() { + use HelpersHandler; + + public function wrongEnhance(): void + { + $this->enhanceTwig(new \stdClass()); + } + }; - new PugSymfonyEngine(self::$kernel); + $object->wrongEnhance(); } /** @@ -131,30 +159,31 @@ public function testRequireTwig() */ public function testPreRenderPhp() { - $kernel = new TestKernel(function (Container $container) { + $kernel = new TestKernel(static function (Container $container) { $container->setParameter('pug', [ 'expressionLanguage' => 'php', ]); }); $kernel->boot(); - $pugSymfony = new PugSymfonyEngine($kernel); + $pugSymfony = $this->getPugSymfonyEngine(); + $pugSymfony->setOption('prettyprint', false); - self::assertSame('

/foo

', $pugSymfony->renderString('p=asset("/foo")')); + self::assertSame('

/foo

', trim($pugSymfony->renderString('p=asset("/foo")'))); self::assertSame( 'My Site

/foo

', - $pugSymfony->render('asset.pug') + $pugSymfony->render('asset.pug'), ); } public function testMixin() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOption('expressionLanguage', 'js'); $pugSymfony->setOption('prettyprint', false); self::assertSame( '', - $pugSymfony->render('mixin.pug') + $pugSymfony->render('mixin.pug'), ); } @@ -163,26 +192,27 @@ public function testMixin() */ public function testPreRenderJs() { - $kernel = new TestKernel(function (Container $container) { + $kernel = new TestKernel(static function (Container $container) { $container->setParameter('pug', [ 'expressionLanguage' => 'js', ]); }); $kernel->boot(); - $pugSymfony = new PugSymfonyEngine($kernel); + $pugSymfony = $this->getPugSymfonyEngine(); - self::assertSame('

/foo

', $pugSymfony->renderString('p=asset("/foo")')); + self::assertSame('

/foo

', trim($pugSymfony->renderString('p=asset("/foo")'))); } public function testPreRenderFile() { - $kernel = new TestKernel(function (Container $container) { + $kernel = new TestKernel(static function (Container $container) { $container->setParameter('pug', [ 'expressionLanguage' => 'js', ]); }); $kernel->boot(); - $pugSymfony = new PugSymfonyEngine($kernel); + $pugSymfony = $this->getPugSymfonyEngine(); + $pugSymfony->setOption('prettyprint', false); self::assertSame(implode('', [ '', @@ -199,14 +229,14 @@ public function testPreRenderFile() */ public function testPreRenderCsrfToken() { - $kernel = new TestKernel(function (Container $container) { + $kernel = new TestKernel(static function (Container $container) { $container->setParameter('pug', [ 'expressionLanguage' => 'js', ]); }); $kernel->boot(); - $pugSymfony = new PugSymfonyEngine($kernel); - $this->addFormRenderer($kernel->getContainer()); + $pugSymfony = $this->getPugSymfonyEngine($kernel); + $this->addFormRenderer(); self::assertSame('

Hello

', $pugSymfony->renderString('p Hello')); @@ -215,7 +245,7 @@ public function testPreRenderCsrfToken() public function testGetEngine() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertInstanceOf(Pug::class, $pugSymfony->getRenderer()); } @@ -229,12 +259,11 @@ public function testSecurityToken() $tokenStorage = new TokenStorage(); $container = self::$kernel->getContainer(); $reflectionProperty = new ReflectionProperty($container, 'services'); - $reflectionProperty->setAccessible(true); $services = $reflectionProperty->getValue($container); $services['security.token_storage'] = $tokenStorage; $reflectionProperty->setValue($container, $services); - $pugSymfony = new PugSymfonyEngine(self::$kernel); - $this->addFormRenderer($container); + $pugSymfony = $this->getPugSymfonyEngine(); + $this->addFormRenderer(); self::assertSame('

the token

', trim($pugSymfony->render('token.pug'))); } @@ -246,26 +275,18 @@ public function testSecurityToken() public function testLogoutHelper() { $generator = new LogoutUrlGenerator(); - /* @var Environment $twig */ - $twig = self::$kernel->getContainer()->get('twig'); + $twig = $this->getTwigEnvironment(); foreach ($twig->getExtensions() as $extension) { if ($extension instanceof LogoutUrlExtension) { $reflectionClass = new \ReflectionClass('Symfony\Bridge\Twig\Extension\LogoutUrlExtension'); $reflectionProperty = $reflectionClass->getProperty('generator'); - $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($extension, $generator); $generator = null; } } - if ($generator) { - include_once __DIR__.'/LogoutUrlHelper.php'; - $logoutUrlHelper = new LogoutUrlHelper($generator); - self::$kernel->getContainer()->set('templating.helper.logout_url', $logoutUrlHelper); - } - - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame('', trim($pugSymfony->render('logout.pug'))); } @@ -275,11 +296,9 @@ public function testLogoutHelper() */ public function testFormHelpers() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); - $container = self::$kernel->getContainer(); - $this->addFormRenderer($container); + $pugSymfony = $this->getPugSymfonyEngine(); + $this->addFormRenderer(); $controller = new TestController(); - $controller->setContainer($container); self::assertRegExp('/^'.implode('', [ '
', @@ -300,11 +319,9 @@ public function testFormHelpers() */ public function testRenderViaTwig() { - $container = self::$kernel->getContainer(); $controller = new TestController(); - $controller->setContainer($container); - /** @var Environment $twig */ - $twig = $container->get('twig'); + $twig = $this->getTwigEnvironment(); + $this->getPugSymfonyEngine(); self::assertInstanceOf(Environment::class, $twig); self::assertInstanceOf(PugSymfonyEngine::class, $twig->getEngine()); @@ -325,10 +342,9 @@ public function testRenderViaTwig() */ public function testServicesSharing() { - /** @var Environment $twig */ - $twig = self::$kernel->getContainer()->get('twig'); - $twig->addGlobal('t', self::$kernel->getContainer()->get('translator')); - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $twig = $this->getTwigEnvironment(); + $twig->addGlobal('t', new Translator('en_US')); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame('

Hello Bob

', trim($pugSymfony->renderString('p=t.trans("Hello %name%", {"%name%": "Bob"})'))); } @@ -338,18 +354,16 @@ public function testServicesSharing() */ public function testTwigGlobals() { - $container = self::$kernel->getContainer(); - $pugSymfony = new PugSymfonyEngine(self::$kernel); - /** @var Environment $twig */ - $twig = $container->get('twig'); + $twig = $this->getTwigEnvironment(); $twig->addGlobal('answer', 42); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame('

42

', trim($pugSymfony->renderString('p=answer'))); } public function testOptions() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOptions(['foo' => 'bar']); self::assertSame('bar', $pugSymfony->getOptionDefault('foo')); @@ -360,7 +374,7 @@ public function testOptions() */ public function testBundleView() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame('

Hello

', trim($pugSymfony->render('TestBundle::bundle.pug', ['text' => 'Hello']))); self::assertSame('
World
', trim($pugSymfony->render('TestBundle:directory:file.pug'))); @@ -371,23 +385,26 @@ public function testBundleView() */ public function testAssetHelperPhp() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOption('expressionLanguage', 'php'); self::assertSame( '
'."\n". '
', preg_replace( '/<', - str_replace(['\'assets/', "\r"], ['\'/assets/', ''], trim($pugSymfony->render('style-php.pug'))) - ) + strtr(trim($pugSymfony->render('style-php.pug')), [ + "\r" => '', + ''' => "'", + ]), + ), ); } @@ -396,23 +413,26 @@ public function testAssetHelperPhp() */ public function testAssetHelperJs() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOption('expressionLanguage', 'js'); self::assertSame( '
'."\n". '
', preg_replace( '/<', - str_replace(['\'assets/', "\r"], ['\'/assets/', ''], trim($pugSymfony->render('style-js.pug'))) - ) + strtr(trim($pugSymfony->render('style-js.pug')), [ + "\r" => '', + ''' => "'", + ]), + ), ); } @@ -421,7 +441,7 @@ public function testAssetHelperJs() */ public function testFilter() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertFalse($pugSymfony->hasFilter('upper')); @@ -439,7 +459,7 @@ public function testFilter() public function testExists() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertTrue($pugSymfony->exists('logout.pug')); self::assertFalse($pugSymfony->exists('login.pug')); @@ -448,7 +468,7 @@ public function testExists() public function testSupports() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertTrue($pugSymfony->supports('foo-bar.pug')); self::assertTrue($pugSymfony->supports('foo-bar.jade')); @@ -461,7 +481,7 @@ public function testSupports() */ public function testCustomOptions() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOptions([ 'prettyprint' => ' ', 'cache' => null, @@ -494,12 +514,11 @@ public function testCustomBaseDir() { $container = self::$kernel->getContainer(); $property = new ReflectionProperty($container, 'parameters'); - $property->setAccessible(true); $value = $property->getValue($container); $value['pug']['prettyprint'] = false; $value['pug']['baseDir'] = __DIR__.'/../project-s5/templates-bis'; $property->setValue($container, $value); - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame( '

', @@ -515,12 +534,11 @@ public function testCustomPaths() { $container = self::$kernel->getContainer(); $property = new ReflectionProperty($container, 'parameters'); - $property->setAccessible(true); $value = $property->getValue($container); $value['pug']['prettyprint'] = false; $value['pug']['paths'] = [__DIR__.'/../project-s5/templates-bis']; $property->setValue($container, $value); - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame( '

alt

', @@ -534,15 +552,16 @@ public function testCustomPaths() */ public function testMissingDir() { + self::expectExceptionObject(new CompilerException( + new SourceLocation('page.pug', 1, 0), + 'Source file page.pug not found', + )); + $kernel = new TestKernel(); $kernel->boot(); $kernel->setProjectDirectory(__DIR__.'/../project'); - $pugSymfony = new PugSymfonyEngine($kernel); - self::assertSame( - '

Page

', - trim($pugSymfony->render('page.pug')) - ); + $this->getPugSymfonyEngine()->render('page.pug'); } public function testForbidThis() @@ -550,7 +569,7 @@ public function testForbidThis() self::expectException(ReservedVariable::class); self::expectExceptionMessage('"this" is a reserved variable name, you can\'t overwrite it.'); - (new PugSymfonyEngine(self::$kernel))->render('p.pug', ['this' => 42]); + $this->getPugSymfonyEngine()->render('p.pug', ['this' => 42]); } public function testForbidBlocks() @@ -558,12 +577,12 @@ public function testForbidBlocks() self::expectException(ReservedVariable::class); self::expectExceptionMessage('"blocks" is a reserved variable name, you can\'t overwrite it.'); - (new PugSymfonyEngine(self::$kernel))->render('p.pug', ['blocks' => 42]); + $this->getPugSymfonyEngine()->render('p.pug', ['blocks' => 42]); } public function testIssue11BackgroundImage() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->setOption('expressionLanguage', 'js'); $html = trim($pugSymfony->render('background-image.pug', ['image' => 'foo'])); $html = preg_replace('/]+)>/', '', $html); @@ -598,17 +617,15 @@ public function testCompileException() self::expectException(RuntimeException::class); self::expectExceptionMessage('Unable to compile void function.'); - new PugSymfonyEngine(self::$kernel); - /** @var Environment $twig */ - $twig = self::$kernel->getContainer()->get('twig'); + $this->getPugSymfonyEngine(); + $twig = $this->getTwigEnvironment(); $twig->compileCode(new TwigFunction('void'), '{# comment #}'); } public function testLoadTemplate() { - new PugSymfonyEngine(self::$kernel); - /** @var Environment $twig */ - $twig = self::$kernel->getContainer()->get('twig'); + $this->getPugSymfonyEngine(); + $twig = $this->getTwigEnvironment(); try { $twig->loadTemplate('a', 'b', 1); @@ -623,7 +640,7 @@ public function testLoadTemplate() public function testDefaultOption() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame(42, $pugSymfony->getOptionDefault('does-not-exist', 42)); @@ -634,7 +651,7 @@ public function testDefaultOption() public function testGetSharedVariables() { - $pugSymfony = new PugSymfonyEngine(self::$kernel); + $pugSymfony = $this->getPugSymfonyEngine(); $pugSymfony->share('foo', 'bar'); self::assertSame('bar', $pugSymfony->getSharedVariables()['foo']); @@ -648,15 +665,11 @@ public function testRenderInterceptor() { $container = self::$kernel->getContainer(); $property = new ReflectionProperty($container, 'parameters'); - $property->setAccessible(true); $value = $property->getValue($container); $value['pug']['interceptors'] = [PugInterceptor::class]; $property->setValue($container, $value); - $controller = new TestController(); - $controller->setContainer($container); - $pugSymfony = new PugSymfonyEngine(self::$kernel); - /** @var Environment $twig */ - $twig = $container->get('twig'); + $twig = $this->getTwigEnvironment(); + $pugSymfony = $this->getPugSymfonyEngine(); self::assertSame(Environment::class, trim($twig->render('new-var.pug'))); diff --git a/tests/Pug/TestCsrfTokenManager.php b/tests/Pug/TestCsrfTokenManager.php index 4a30891..2deb39c 100644 --- a/tests/Pug/TestCsrfTokenManager.php +++ b/tests/Pug/TestCsrfTokenManager.php @@ -13,7 +13,7 @@ public function __construct(TokenGeneratorInterface $generator = null, TokenStor { } - public function getToken(string $tokenId) + public function getToken(string $tokenId): CsrfToken { if ($tokenId === 'special') { return new CsrfToken('special', 'the token'); diff --git a/tests/Pug/TestKernel.php b/tests/Pug/TestKernel.php index f97826f..20030c8 100644 --- a/tests/Pug/TestKernel.php +++ b/tests/Pug/TestKernel.php @@ -9,19 +9,15 @@ class TestKernel extends Kernel { - /** - * @var Closure - */ - private $containerConfigurator; + private Closure $containerConfigurator; - /** - * @var string - */ - private $projectDirectory; + private ?string $projectDirectory = null; + + private string $rootDir; public function __construct(Closure $containerConfigurator = null, $environment = 'test', $debug = false) { - $this->containerConfigurator = $containerConfigurator ?? function () { + $this->containerConfigurator = $containerConfigurator ?? static function () { }; parent::__construct($environment, $debug); diff --git a/tests/project-s5/config/packages/framework.yaml b/tests/project-s5/config/packages/framework.yaml index e408b96..05a4fa3 100644 --- a/tests/project-s5/config/packages/framework.yaml +++ b/tests/project-s5/config/packages/framework.yaml @@ -6,7 +6,7 @@ framework: csrf_protection: true validation: { enable_annotations: true } session: - storage_id: session.storage.filesystem + handler_id: null parameters: pug: diff --git a/tests/project-s5/config/packages/security.yaml b/tests/project-s5/config/packages/security.yaml index 375954b..cbb3e9a 100644 --- a/tests/project-s5/config/packages/security.yaml +++ b/tests/project-s5/config/packages/security.yaml @@ -3,7 +3,6 @@ security: in_memory: { memory: ~ } firewalls: main: - anonymous: ~ logout: path: /logout target: / \ No newline at end of file diff --git a/tests/project-s5/src/Controller/DefaultController.php b/tests/project-s5/src/Controller/DefaultController.php index f9ac889..8867b62 100644 --- a/tests/project-s5/src/Controller/DefaultController.php +++ b/tests/project-s5/src/Controller/DefaultController.php @@ -6,18 +6,12 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Security\Csrf\CsrfTokenManager; -use Twig\Environment; use Twig\RuntimeLoader\FactoryRuntimeLoader; class DefaultController extends AbstractController { protected $twig; -// public function __construct(Environment $twig) -// { -// $this->twig = $twig; -// } - public function index() { $defaultFormTheme = 'form_div_layout.html.twig'; diff --git a/tests/project-s5/src/Service/PugInterceptor.php b/tests/project-s5/src/Service/PugInterceptor.php index c36b6dc..0704207 100644 --- a/tests/project-s5/src/Service/PugInterceptor.php +++ b/tests/project-s5/src/Service/PugInterceptor.php @@ -2,28 +2,22 @@ namespace App\Service; +use Pug\PugSymfonyEngine; use Pug\Symfony\Contracts\InterceptorInterface; use Pug\Symfony\RenderEvent; use Symfony\Contracts\EventDispatcher\Event; -use Twig\Environment; class PugInterceptor implements InterceptorInterface { - /** - * @var Environment - */ - private $twig; - - public function __construct(Environment $twig) + public function __construct(private PugSymfonyEngine $pug) { - $this->twig = $twig; } public function intercept(Event $event) { if ($event instanceof RenderEvent) { $locals = $event->getLocals(); - $locals['newVar'] = get_class($this->twig); + $locals['newVar'] = get_class($this->pug->getTwig()); $event->setLocals($locals); if ($event->getEngine()->getOptionDefault('special-thing', false)) { diff --git a/tests/project-s5/templates/form_div_layout.html.twig b/tests/project-s5/templates/form_div_layout.html.twig new file mode 100644 index 0000000..61f64e3 --- /dev/null +++ b/tests/project-s5/templates/form_div_layout.html.twig @@ -0,0 +1,484 @@ +{# Widgets #} + +{%- block form_widget -%} + {% if compound %} + {{- block('form_widget_compound') -}} + {% else %} + {{- block('form_widget_simple') -}} + {% endif %} +{%- endblock form_widget -%} + +{%- block form_widget_simple -%} + {%- set type = type|default('text') -%} + {%- if type == 'range' or type == 'color' -%} + {# Attribute "required" is not supported #} + {%- set required = false -%} + {%- endif -%} + +{%- endblock form_widget_simple -%} + +{%- block form_widget_compound -%} +
+ {%- if form is rootform -%} + {{ form_errors(form) }} + {%- endif -%} + {{- block('form_rows') -}} + {{- form_rest(form) -}} +
+{%- endblock form_widget_compound -%} + +{%- block collection_widget -%} + {% if prototype is defined and not prototype.rendered %} + {%- set attr = attr|merge({'data-prototype': form_row(prototype) }) -%} + {% endif %} + {{- block('form_widget') -}} +{%- endblock collection_widget -%} + +{%- block textarea_widget -%} + +{%- endblock textarea_widget -%} + +{%- block choice_widget -%} + {% if expanded %} + {{- block('choice_widget_expanded') -}} + {% else %} + {{- block('choice_widget_collapsed') -}} + {% endif %} +{%- endblock choice_widget -%} + +{%- block choice_widget_expanded -%} +
+ {%- for child in form %} + {{- form_widget(child) -}} + {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}} + {% endfor -%} +
+{%- endblock choice_widget_expanded -%} + +{%- block choice_widget_collapsed -%} + {%- if required and placeholder is none and not placeholder_in_choices and not multiple and (attr.size is not defined or attr.size <= 1) -%} + {% set required = false %} + {%- endif -%} + +{%- endblock choice_widget_collapsed -%} + +{%- block choice_widget_options -%} + {% for group_label, choice in options %} + {%- if choice is iterable -%} + + {% set options = choice %} + {{- block('choice_widget_options') -}} + + {%- else -%} + + {%- endif -%} + {% endfor %} +{%- endblock choice_widget_options -%} + +{%- block checkbox_widget -%} + +{%- endblock checkbox_widget -%} + +{%- block radio_widget -%} + +{%- endblock radio_widget -%} + +{%- block datetime_widget -%} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {%- else -%} +
+ {{- form_errors(form.date) -}} + {{- form_errors(form.time) -}} + {{- form_widget(form.date) -}} + {{- form_widget(form.time) -}} +
+ {%- endif -%} +{%- endblock datetime_widget -%} + +{%- block date_widget -%} + {%- if widget == 'single_text' -%} + {{ block('form_widget_simple') }} + {%- else -%} +
+ {{- date_pattern|replace({ + '{{ year }}': form_widget(form.year), + '{{ month }}': form_widget(form.month), + '{{ day }}': form_widget(form.day), + })|raw -}} +
+ {%- endif -%} +{%- endblock date_widget -%} + +{%- block time_widget -%} + {%- if widget == 'single_text' -%} + {{ block('form_widget_simple') }} + {%- else -%} + {%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%} +
+ {{ form_widget(form.hour, vars) }}{% if with_minutes %}:{{ form_widget(form.minute, vars) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second, vars) }}{% endif %} +
+ {%- endif -%} +{%- endblock time_widget -%} + +{%- block dateinterval_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} +
+ {{- form_errors(form) -}} + + + + {%- if with_years %}{% endif -%} + {%- if with_months %}{% endif -%} + {%- if with_weeks %}{% endif -%} + {%- if with_days %}{% endif -%} + {%- if with_hours %}{% endif -%} + {%- if with_minutes %}{% endif -%} + {%- if with_seconds %}{% endif -%} + + + + + {%- if with_years %}{% endif -%} + {%- if with_months %}{% endif -%} + {%- if with_weeks %}{% endif -%} + {%- if with_days %}{% endif -%} + {%- if with_hours %}{% endif -%} + {%- if with_minutes %}{% endif -%} + {%- if with_seconds %}{% endif -%} + + + + {%- if with_invert %}{{ form_widget(form.invert) }}{% endif -%} +
+ {%- endif -%} +{%- endblock dateinterval_widget -%} + +{%- block number_widget -%} + {# type="number" doesn't work with floats in localized formats #} + {%- set type = type|default('text') -%} + {{ block('form_widget_simple') }} +{%- endblock number_widget -%} + +{%- block integer_widget -%} + {%- set type = type|default('number') -%} + {{ block('form_widget_simple') }} +{%- endblock integer_widget -%} + +{%- block money_widget -%} + {{ money_pattern|form_encode_currency(block('form_widget_simple')) }} +{%- endblock money_widget -%} + +{%- block url_widget -%} + {%- set type = type|default('url') -%} + {{ block('form_widget_simple') }} +{%- endblock url_widget -%} + +{%- block search_widget -%} + {%- set type = type|default('search') -%} + {{ block('form_widget_simple') }} +{%- endblock search_widget -%} + +{%- block percent_widget -%} + {%- set type = type|default('text') -%} + {{ block('form_widget_simple') }}{% if symbol %} {{ symbol|default('%') }}{% endif %} +{%- endblock percent_widget -%} + +{%- block password_widget -%} + {%- set type = type|default('password') -%} + {{ block('form_widget_simple') }} +{%- endblock password_widget -%} + +{%- block hidden_widget -%} + {%- set type = type|default('hidden') -%} + {{ block('form_widget_simple') }} +{%- endblock hidden_widget -%} + +{%- block email_widget -%} + {%- set type = type|default('email') -%} + {{ block('form_widget_simple') }} +{%- endblock email_widget -%} + +{%- block range_widget -%} + {% set type = type|default('range') %} + {{- block('form_widget_simple') -}} +{%- endblock range_widget %} + +{%- block button_widget -%} + {%- if label is empty -%} + {%- if label_format is not empty -%} + {% set label = label_format|replace({ + '%name%': name, + '%id%': id, + }) %} + {%- elseif label is not same as(false) -%} + {% set label = name|humanize %} + {%- endif -%} + {%- endif -%} + +{%- endblock button_widget -%} + +{%- block submit_widget -%} + {%- set type = type|default('submit') -%} + {{ block('button_widget') }} +{%- endblock submit_widget -%} + +{%- block reset_widget -%} + {%- set type = type|default('reset') -%} + {{ block('button_widget') }} +{%- endblock reset_widget -%} + +{%- block tel_widget -%} + {%- set type = type|default('tel') -%} + {{ block('form_widget_simple') }} +{%- endblock tel_widget -%} + +{%- block color_widget -%} + {%- set type = type|default('color') -%} + {{ block('form_widget_simple') }} +{%- endblock color_widget -%} + +{%- block week_widget -%} + {%- if widget == 'single_text' -%} + {{ block('form_widget_simple') }} + {%- else -%} + {%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%} +
+ {{ form_widget(form.year, vars) }}-{{ form_widget(form.week, vars) }} +
+ {%- endif -%} +{%- endblock week_widget -%} + +{# Labels #} + +{%- block form_label -%} + {% if label is not same as(false) -%} + {% if not compound -%} + {% set label_attr = label_attr|merge({'for': id}) %} + {%- endif -%} + {% if required -%} + {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} + {%- endif -%} + <{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}> + {{- block('form_label_content') -}} + + {%- endif -%} +{%- endblock form_label -%} + +{%- block form_label_content -%} + {%- if label is empty -%} + {%- if label_format is not empty -%} + {% set label = label_format|replace({ + '%name%': name, + '%id%': id, + }) %} + {%- else -%} + {% set label = name|humanize %} + {%- endif -%} + {%- endif -%} + {%- if translation_domain is same as(false) -%} + {%- if label_html is same as(false) -%} + {{- label -}} + {%- else -%} + {{- label|raw -}} + {%- endif -%} + {%- else -%} + {%- if label_html is same as(false) -%} + {{- label|trans(label_translation_parameters, translation_domain) -}} + {%- else -%} + {{- label|trans(label_translation_parameters, translation_domain)|raw -}} + {%- endif -%} + {%- endif -%} +{%- endblock form_label_content -%} + +{%- block button_label -%}{%- endblock -%} + +{# Help #} + +{% block form_help -%} + {%- if help is not empty -%} + {%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ ' help-text')|trim}) -%} +
+ {{- block('form_help_content') -}} +
+ {%- endif -%} +{%- endblock form_help %} + +{% block form_help_content -%} + {%- if translation_domain is same as(false) -%} + {%- if help_html is same as(false) -%} + {{- help -}} + {%- else -%} + {{- help|raw -}} + {%- endif -%} + {%- else -%} + {%- if help_html is same as(false) -%} + {{- help|trans(help_translation_parameters, translation_domain) -}} + {%- else -%} + {{- help|trans(help_translation_parameters, translation_domain)|raw -}} + {%- endif -%} + {%- endif -%} +{%- endblock form_help_content %} + +{# Rows #} + +{%- block repeated_row -%} + {# + No need to render the errors here, as all errors are mapped + to the first child (see RepeatedTypeValidatorExtension). + #} + {{- block('form_rows') -}} +{%- endblock repeated_row -%} + +{%- block form_row -%} + {%- set widget_attr = {} -%} + {%- if help is not empty -%} + {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} + {%- endif -%} + + {{- form_label(form) -}} + {{- form_errors(form) -}} + {{- form_widget(form, widget_attr) -}} + {{- form_help(form) -}} + +{%- endblock form_row -%} + +{%- block button_row -%} + + {{- form_widget(form) -}} + +{%- endblock button_row -%} + +{%- block hidden_row -%} + {{ form_widget(form) }} +{%- endblock hidden_row -%} + +{# Misc #} + +{%- block form -%} + {{ form_start(form) }} + {{- form_widget(form) -}} + {{ form_end(form) }} +{%- endblock form -%} + +{%- block form_start -%} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ["GET", "POST"] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = "POST" %} + {%- endif -%} + + {%- if form_method != method -%} + + {%- endif -%} +{%- endblock form_start -%} + +{%- block form_end -%} + {%- if not render_rest is defined or render_rest -%} + {{ form_rest(form) }} + {%- endif -%} + +{%- endblock form_end -%} + +{%- block form_errors -%} + {%- if errors|length > 0 -%} +
    + {%- for error in errors -%} +
  • {{ error.message }}
  • + {%- endfor -%} +
+ {%- endif -%} +{%- endblock form_errors -%} + +{%- block form_rest -%} + {% for child in form -%} + {% if not child.rendered %} + {{- form_row(child) -}} + {% endif %} + {%- endfor -%} + + {% if not form.methodRendered and form is rootform %} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ["GET", "POST"] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = "POST" %} + {%- endif -%} + + {%- if form_method != method -%} + + {%- endif -%} + {% endif -%} +{% endblock form_rest %} + +{# Support #} + +{%- block form_rows -%} + {% for child in form|filter(child => not child.rendered) %} + {{- form_row(child) -}} + {% endfor %} +{%- endblock form_rows -%} + +{%- block widget_attributes -%} + id="{{ id }}" name="{{ full_name }}" + {%- if disabled %} disabled="disabled"{% endif -%} + {%- if required %} required="required"{% endif -%} + {{ block('attributes') }} +{%- endblock widget_attributes -%} + +{%- block widget_container_attributes -%} + {%- if id is not empty %}id="{{ id }}"{% endif -%} + {{ block('attributes') }} +{%- endblock widget_container_attributes -%} + +{%- block button_attributes -%} + id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif -%} + {{ block('attributes') }} +{%- endblock button_attributes -%} + +{% block attributes -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain) }}" + {%- elseif attrvalue is same as(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not same as(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} +{%- endblock attributes -%}