Skip to content

Commit

Permalink
Use the Symfony assets component for the Contao assets (see #1165).
Browse files Browse the repository at this point in the history
Description
-----------

I started using Symfony Encore for our application asset management and stumbled over the Symfony Asset service. I think there are a ton of useful feature for Contao.

 - It *knows* the path to an asset by asking for the composer package.
 - It automatically adds the `TL_ASSETS_URL` prefix if configured.
 - It automatically adds the package version to the URL, which results in clever cache busting on updates.

The provided code is totally working, but there are a lot more features we can implement. Missing **TODOs**:
 - [ ] Use asset URLs for `TL_JAVASCRIPT` and `TL_CSS`
 - [ ] Add support for asset URLs (absolute) to the `Contao\Combiner`
 - [x] Re-implement `Controller::setStaticUrls` instead of using constants in the context service.
 - [x] *Maybe* add `Resources/public` of all bundles as packages.
 - [ ] *Maybe* add a package for the backend theme
 - [x] Unit Tests

Commits
-------

031904d Use assets service for scripts in templates
5a0b017 Add Contao components as asset packages
51bf9fc Added asset insert tag listener
4a3ca8e Added dependency for terminal42/asset-bundle
ac6f255 Use page model in asset contexts and rewrite legacy method to use them
4499530 Move asset-bundle functionality to internal compiler pass
7d84fc4 Correctly name the asset listener
69ef93d Added full unit tests coverage
d6495f9 Use the global page object instead of setting page model on asset context
4bb7d55 Fix the coding style.
cca8353 Fix some minor issues.
2dcb725 Add a change log entry.
  • Loading branch information
aschempp authored and leofeyer committed Nov 15, 2017
1 parent 265c3c9 commit eed0aea
Show file tree
Hide file tree
Showing 34 changed files with 1,107 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### DEV

* Use the Symfony assets component for the Contao assets (see #1165).
* Do not log known exceptions with a pretty error screen (see #1139).

### 4.5.0-beta1 (2017-11-06)
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"ext-dom": "*",
"ext-gd": "*",
"ext-pcre": "*",
"symfony/asset": "^3.4",
"symfony/console": "^3.4",
"symfony/dependency-injection": "^3.4",
"symfony/filesystem": "^3.4",
Expand Down
132 changes: 132 additions & 0 deletions src/Asset/ContaoContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* Copyright (c) 2005-2017 Leo Feyer
*
* @license LGPL-3.0+
*/

namespace Contao\CoreBundle\Asset;

use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFrameworkInterface;
use Contao\PageModel;
use Symfony\Component\Asset\Context\ContextInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class ContaoContext implements ContextInterface
{
/**
* @var ContaoFrameworkInterface
*/
private $framework;

/**
* @var RequestStack
*/
private $requestStack;

/**
* @var string
*/
private $field;

/**
* @var bool
*/
private $debug;

/**
* @param ContaoFrameworkInterface $framework
* @param RequestStack $requestStack
* @param string $field
* @param bool $debug
*/
public function __construct(ContaoFrameworkInterface $framework, RequestStack $requestStack, string $field, bool $debug = false)
{
$this->framework = $framework;
$this->requestStack = $requestStack;
$this->field = $field;
$this->debug = $debug;
}

/**
* {@inheritdoc}
*/
public function getBasePath()
{
if ($this->debug) {
return '';
}

$request = $this->requestStack->getCurrentRequest();

if (null === $request || '' === ($staticUrl = $this->getFieldValue($this->getPage()))) {
return '';
}

$protocol = $this->isSecure() ? 'https' : 'http';
$relative = preg_replace('@https?://@', '', $staticUrl);

return sprintf('%s://%s%s', $protocol, $relative, $request->getBasePath());
}

/**
* {@inheritdoc}
*/
public function isSecure(): bool
{
$page = $this->getPage();

if (null !== $page) {
return (bool) $page->loadDetails()->rootUseSSL;
}

$request = $this->requestStack->getCurrentRequest();

if (null === $request) {
return false;
}

return $request->isSecure();
}

/**
* Gets the current page model.
*
* @return PageModel|null
*/
private function getPage(): ?PageModel
{
if (isset($GLOBALS['objPage']) && $GLOBALS['objPage'] instanceof PageModel) {
return $GLOBALS['objPage'];
}

return null;
}

/**
* Gets field value from page model or global config.
*
* @param PageModel|null $page
*
* @return string
*/
private function getFieldValue(?PageModel $page): string
{
if (null !== $page) {
return (string) $page->{$this->field};
}

$this->framework->initialize();

/** @var Config $config */
$config = $this->framework->getAdapter(Config::class);

return (string) $config->get($this->field);
}
}
4 changes: 4 additions & 0 deletions src/ContaoCoreBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Contao\CoreBundle;

use Contao\CoreBundle\DependencyInjection\Compiler\AddAssetsPackagesPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddImagineClassPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddPackagesPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddResourcesPathsPass;
Expand Down Expand Up @@ -57,6 +58,9 @@ public function build(ContainerBuilder $container): void
new AddPackagesPass($container->getParameter('kernel.root_dir').'/../vendor/composer/installed.json')
);

// Add the assets packages after the Composer packages
$container->addCompilerPass(new AddAssetsPackagesPass());

$container->addCompilerPass(new AddSessionBagsPass());
$container->addCompilerPass(new AddResourcesPathsPass());
$container->addCompilerPass(new AddImagineClassPass());
Expand Down
166 changes: 166 additions & 0 deletions src/DependencyInjection/Compiler/AddAssetsPackagesPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* Copyright (c) 2005-2017 Leo Feyer
*
* @license LGPL-3.0+
*/

namespace Contao\CoreBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class AddAssetsPackagesPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('assets.packages')) {
return;
}

$this->addBundles($container);
$this->addComponents($container);
}

/**
* Adds every bundle with a public folder as assets package.
*
* @param ContainerBuilder $container
*/
private function addBundles(ContainerBuilder $container): void
{
$packages = $container->getDefinition('assets.packages');
$context = new Reference('contao.assets.plugins_context');

if ($container->hasDefinition('assets._version_default')) {
$version = new Reference('assets._version_default');
} else {
$version = new Reference('assets.empty_version_strategy');
}

/** @var Bundle[] $bundles */
$bundles = $container->get('kernel')->getBundles();

foreach ($bundles as $bundle) {
if (!is_dir($originDir = $bundle->getPath().'/Resources/public')) {
continue;
}

if ($extension = $bundle->getContainerExtension()) {
$packageName = $extension->getAlias();
} else {
$packageName = $this->getBundlePackageName($bundle);
}

$serviceId = 'assets._package_'.$packageName;
$basePath = 'bundles/'.preg_replace('/bundle$/', '', strtolower($bundle->getName()));

$container->setDefinition($serviceId, $this->createPackageDefinition($basePath, $version, $context));
$packages->addMethodCall('addPackage', [$packageName, new Reference($serviceId)]);
}
}

/**
* Adds the Contao components as assets packages.
*
* @param ContainerBuilder $container
*/
private function addComponents(ContainerBuilder $container): void
{
if (!$container->hasParameter('kernel.packages')) {
return;
}

$packages = $container->getDefinition('assets.packages');
$context = new Reference('contao.assets.plugins_context');
$components = $container->getParameter('kernel.packages');

foreach ($components as $name => $version) {
[$vendor, $packageName] = explode('/', $name, 2);

if ('contao-components' !== $vendor) {
continue;
}

$serviceId = 'assets._package_'.$name;
$basePath = 'assets/'.$packageName;
$version = $this->createPackageVersion($container, $version, $name);

$container->setDefinition($serviceId, $this->createPackageDefinition($basePath, $version, $context));
$packages->addMethodCall('addPackage', [$name, new Reference($serviceId)]);
}
}

/**
* Creates an assets package definition.
*
* @param string $basePath
* @param Reference $version
* @param Reference $context
*
* @return Definition
*/
private function createPackageDefinition(string $basePath, Reference $version, Reference $context): Definition
{
$package = new ChildDefinition('assets.path_package');

$package
->setPublic(false)
->replaceArgument(0, $basePath)
->replaceArgument(1, $version)
->replaceArgument(2, $context)
;

return $package;
}

/**
* Creates an asset package version strategy.
*
* @param ContainerBuilder $container
* @param string $version
* @param string $name
*
* @return Reference
*/
private function createPackageVersion(ContainerBuilder $container, string $version, string $name): Reference
{
$def = new ChildDefinition('assets.static_version_strategy');
$def->replaceArgument(0, $version);

$container->setDefinition('assets._version_'.$name, $def);

return new Reference('assets._version_'.$name);
}

/**
* Returns a bundle package name emulating what a bundle extension would look like.
*
* @param Bundle $bundle
*
* @return string
*/
private function getBundlePackageName(Bundle $bundle): string
{
$className = $bundle->getName();

if ('Bundle' === substr($className, -6)) {
$className = substr($className, 0, -6);
}

return Container::underscore($className);
}
}
49 changes: 49 additions & 0 deletions src/EventListener/InsertTags/AssetListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* Copyright (c) 2005-2017 Leo Feyer
*
* @license LGPL-3.0+
*/

namespace Contao\CoreBundle\EventListener\InsertTags;

use Symfony\Component\Asset\Packages;

class AssetListener
{
/**
* @var Packages
*/
private $packages;

/**
* @param Packages $packages
*/
public function __construct(Packages $packages)
{
$this->packages = $packages;
}

/**
* Replaces the "asset" insert tag.
*
* @param string $tag
*
* @return string|false
*/
public function onReplaceInsertTags(string $tag)
{
$chunks = explode('::', $tag);

if ('asset' === $chunks[0]) {
return $this->packages->getUrl($chunks[1], $chunks[2] ?? null);
}

return false;
}
}
7 changes: 7 additions & 0 deletions src/Resources/config/listener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ services:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

contao.listener.insert_tags.asset:
class: Contao\CoreBundle\EventListener\InsertTags\AssetListener
arguments:
- "@assets.packages"
tags:
- { name: contao.hook, hook: replaceInsertTags }

contao.listener.locale:
class: Contao\CoreBundle\EventListener\LocaleListener
arguments:
Expand Down
Loading

0 comments on commit eed0aea

Please sign in to comment.