From d33c41d43663f1f638ec083fa2b8c3bddec5ad1a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 28 Dec 2014 19:57:17 +0100 Subject: [PATCH] [Asset] added the component --- composer.json | 1 + src/Symfony/Bridge/Twig/CHANGELOG.md | 1 + .../Bridge/Twig/Extension/AssetExtension.php | 79 +++++++++ src/Symfony/Bridge/Twig/composer.json | 1 + .../Compiler/TemplatingAssetHelperPass.php | 3 + .../DependencyInjection/Configuration.php | 55 +++++- .../FrameworkExtension.php | 84 +++++++++ .../Resources/config/assets.xml | 42 +++++ .../Templating/Asset/PackageFactory.php | 4 + .../Templating/Asset/PathPackage.php | 4 + .../Bundle/FrameworkBundle/composer.json | 1 + src/Symfony/Bundle/TwigBundle/CHANGELOG.md | 6 + .../Compiler/ExtensionPass.php | 4 + .../TwigBundle/Extension/AssetsExtension.php | 6 + .../TwigBundle/Resources/config/twig.xml | 4 + ...Test.php => LegacyAssetsExtensionTest.php} | 2 +- src/Symfony/Component/Asset/.gitignore | 3 + src/Symfony/Component/Asset/CHANGELOG.md | 7 + .../Asset/Context/ContextInterface.php | 34 ++++ .../Asset/Context/RequestStackContext.php | 53 ++++++ .../Asset/Exception/ExceptionInterface.php | 21 +++ .../Exception/InvalidArgumentException.php | 21 +++ .../Asset/Exception/LogicException.php | 21 +++ src/Symfony/Component/Asset/LICENSE | 19 ++ src/Symfony/Component/Asset/Package.php | 75 ++++++++ .../Component/Asset/PackageInterface.php | 38 ++++ src/Symfony/Component/Asset/Packages.php | 116 ++++++++++++ src/Symfony/Component/Asset/PathPackage.php | 74 ++++++++ src/Symfony/Component/Asset/README.md | 166 ++++++++++++++++++ .../Component/Asset/Tests/PackageTest.php | 54 ++++++ .../Component/Asset/Tests/PackagesTest.php | 76 ++++++++ .../Component/Asset/Tests/PathPackageTest.php | 84 +++++++++ .../Component/Asset/Tests/UrlPackageTest.php | 104 +++++++++++ src/Symfony/Component/Asset/UrlPackage.php | 134 ++++++++++++++ .../VersionStrategy/EmptyVersionStrategy.php | 36 ++++ .../VersionStrategy/StaticVersionStrategy.php | 51 ++++++ .../VersionStrategyInterface.php | 38 ++++ src/Symfony/Component/Asset/composer.json | 36 ++++ src/Symfony/Component/Asset/phpunit.xml.dist | 24 +++ .../Component/Templating/Asset/Package.php | 4 + .../Templating/Asset/PackageInterface.php | 4 + .../Templating/Asset/PathPackage.php | 4 + .../Component/Templating/Asset/UrlPackage.php | 4 + .../Templating/Helper/AssetsHelper.php | 4 + .../Templating/Helper/CoreAssetsHelper.php | 4 + ...perTest.php => LegacyAssetsHelperTest.php} | 2 +- ...est.php => LegacyCoreAssetsHelperTest.php} | 2 +- 47 files changed, 1605 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Extension/AssetExtension.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml rename src/Symfony/Bundle/TwigBundle/Tests/Extension/{AssetsExtensionTest.php => LegacyAssetsExtensionTest.php} (98%) create mode 100644 src/Symfony/Component/Asset/.gitignore create mode 100644 src/Symfony/Component/Asset/CHANGELOG.md create mode 100644 src/Symfony/Component/Asset/Context/ContextInterface.php create mode 100644 src/Symfony/Component/Asset/Context/RequestStackContext.php create mode 100644 src/Symfony/Component/Asset/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/Asset/Exception/InvalidArgumentException.php create mode 100644 src/Symfony/Component/Asset/Exception/LogicException.php create mode 100644 src/Symfony/Component/Asset/LICENSE create mode 100644 src/Symfony/Component/Asset/Package.php create mode 100644 src/Symfony/Component/Asset/PackageInterface.php create mode 100644 src/Symfony/Component/Asset/Packages.php create mode 100644 src/Symfony/Component/Asset/PathPackage.php create mode 100644 src/Symfony/Component/Asset/README.md create mode 100644 src/Symfony/Component/Asset/Tests/PackageTest.php create mode 100644 src/Symfony/Component/Asset/Tests/PackagesTest.php create mode 100644 src/Symfony/Component/Asset/Tests/PathPackageTest.php create mode 100644 src/Symfony/Component/Asset/Tests/UrlPackageTest.php create mode 100644 src/Symfony/Component/Asset/UrlPackage.php create mode 100644 src/Symfony/Component/Asset/VersionStrategy/EmptyVersionStrategy.php create mode 100644 src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php create mode 100644 src/Symfony/Component/Asset/VersionStrategy/VersionStrategyInterface.php create mode 100644 src/Symfony/Component/Asset/composer.json create mode 100644 src/Symfony/Component/Asset/phpunit.xml.dist rename src/Symfony/Component/Templating/Tests/Helper/{AssetsHelperTest.php => LegacyAssetsHelperTest.php} (98%) rename src/Symfony/Component/Templating/Tests/Helper/{CoreAssetsHelperTest.php => LegacyCoreAssetsHelperTest.php} (94%) diff --git a/composer.json b/composer.json index b123e37c78..5a04094969 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "psr/log": "~1.0" }, "replace": { + "symfony/asset": "self.version", "symfony/browser-kit": "self.version", "symfony/class-loader": "self.version", "symfony/config": "self.version", diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 887e9acf34..9da097f7b0 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * added LogoutUrlExtension (provides `logout_url` and `logout_path`) * added an HttpFoundation extension (provides the `absolute_url` and the `relative_path` functions) + * added AssetExtension (provides the `asset_path` function) 2.5.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php new file mode 100644 index 0000000000..3f8bb93c3e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\Asset\Packages; + +/** + * Twig extension for the Symfony Asset component. + * + * @author Fabien Potencier + */ +class AssetExtension extends \Twig_Extension +{ + private $packages; + + public function __construct(Packages $packages) + { + $this->packages = $packages; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('asset_path', array($this, 'getAssetPath')), + new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')), + ); + } + + /** + * Returns the public path of an asset. + * + * If the package used to generate the path is an instance of + * UrlPackage, you will always get a URL and not a path. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The public path of the asset + */ + public function getAssetPath($path, $packageName = null) + { + return $this->packages->getUrl($path, $packageName); + } + + /** + * Returns the version of an asset. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The asset version + */ + public function getAssetVersion($path, $packageName = null) + { + return $this->packages->getVersion($path, $packageName); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'asset'; + } +} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index c534b8f6ac..df8a513113 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -37,6 +37,7 @@ }, "suggest": { "symfony/finder": "", + "symfony/asset": "For using the AssetExtension", "symfony/form": "For using the FormExtension", "symfony/http-kernel": "For using the HttpKernelExtension", "symfony/routing": "For using the RoutingExtension", diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php index 39a4e13bbd..6c0ff55114 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php @@ -16,6 +16,9 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +/** + * @deprecated since 2.7, will be removed in 3.0 + */ class TemplatingAssetHelperPass implements CompilerPassInterface { public function process(ContainerBuilder $container) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 105326112c..c08e177efb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -108,6 +108,7 @@ class Configuration implements ConfigurationInterface $this->addSessionSection($rootNode); $this->addRequestSection($rootNode); $this->addTemplatingSection($rootNode); + $this->addAssetsSection($rootNode); $this->addTranslatorSection($rootNode); $this->addValidationSection($rootNode); $this->addAnnotationsSection($rootNode); @@ -347,8 +348,8 @@ class Configuration implements ConfigurationInterface ->info('templating configuration') ->canBeUnset() ->children() - ->scalarNode('assets_version')->defaultValue(null)->end() - ->scalarNode('assets_version_format')->defaultValue('%%s?%%s')->end() + ->scalarNode('assets_version')->defaultNull()->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.')->end() + ->scalarNode('assets_version_format')->defaultValue('%%s?%%s')->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.')->end() ->scalarNode('hinclude_default_template')->defaultNull()->end() ->arrayNode('form') ->addDefaultsIfNotSet() @@ -370,6 +371,7 @@ class Configuration implements ConfigurationInterface ->fixXmlConfig('assets_base_url') ->children() ->arrayNode('assets_base_urls') + ->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.') ->performNoDeepMerging() ->addDefaultsIfNotSet() ->beforeNormalization() @@ -417,6 +419,7 @@ class Configuration implements ConfigurationInterface ->fixXmlConfig('package') ->children() ->arrayNode('packages') + ->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.') ->useAttributeAsKey('name') ->prototype('array') ->fixXmlConfig('base_url') @@ -452,6 +455,54 @@ class Configuration implements ConfigurationInterface ; } + private function addAssetsSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('assets') + ->info('assets configuration') + ->canBeUnset() + ->fixXmlConfig('base_url') + ->children() + ->scalarNode('version')->defaultNull()->end() + ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() + ->scalarNode('base_path')->defaultValue('')->end() + ->arrayNode('base_urls') + ->requiresAtLeastOneElement() + ->beforeNormalization() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->fixXmlConfig('package') + ->children() + ->arrayNode('packages') + ->useAttributeAsKey('name') + ->prototype('array') + ->fixXmlConfig('base_url') + ->children() + ->scalarNode('version')->defaultNull()->end() + ->scalarNode('version_format')->defaultNull()->end() + ->scalarNode('base_path')->defaultValue('')->end() + ->arrayNode('base_urls') + ->requiresAtLeastOneElement() + ->beforeNormalization() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + private function addTranslatorSection(ArrayNodeDefinition $rootNode) { $rootNode diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 1a38962f16..22ac7d34c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -101,6 +101,10 @@ class FrameworkExtension extends Extension $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); + if (isset($config['assets'])) { + $this->registerAssetsConfiguration($config['assets'], $container, $loader); + } + if (isset($config['templating'])) { $this->registerTemplatingConfiguration($config['templating'], $config['ide'], $container, $loader); } @@ -632,6 +636,86 @@ class FrameworkExtension extends Extension return $package; } + /** + * Loads the assets configuration. + * + * @param array $config A assets configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance + */ + private function registerAssetsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + $loader->load('assets.xml'); + + $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format']); + + $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); + $container->setDefinition('assets._default_package', $defaultPackage); + + $namedPackages = array(); + foreach ($config['packages'] as $name => $package) { + if (null === $package['version']) { + $version = $defaultVersion; + } else { + $format = $package['version_format'] ?: $config['version_format']; + + $version = $this->createVersion($container, $package['version'], $format, $name); + } + + $container->setDefinition('assets._package_'.$name, $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version)); + $namedPackages[$name] = new Reference('assets._package_'.$name); + } + + $container->getDefinition('assets.packages') + ->replaceArgument(0, new Reference('assets._default_package')) + ->replaceArgument(1, $namedPackages) + ; + } + + /** + * Returns a definition for an asset package. + */ + private function createPackageDefinition($basePath, array $baseUrls, Reference $version) + { + if ($basePath && $baseUrls) { + throw new \LogicException('An asset package cannot have base URLs and base paths.'); + } + + if (!$baseUrls) { + $package = new DefinitionDecorator('assets.path_package'); + + return $package + ->setPublic(false) + ->replaceArgument(0, $basePath) + ->replaceArgument(1, $version) + ; + } + + $package = new DefinitionDecorator('assets.url_package'); + + return $package + ->setPublic(false) + ->replaceArgument(0, $baseUrls) + ->replaceArgument(1, $version) + ; + } + + private function createVersion(ContainerBuilder $container, $version, $format, $name = null) + { + if (!$version) { + return new Reference('assets.empty_version_strategy'); + } + + $def = new DefinitionDecorator('assets.static_version_strategy'); + $def + ->replaceArgument(0, $version) + ->replaceArgument(1, $format) + ; + $container->setDefinition('assets._version_'.$name, $def); + + return new Reference('assets._version_'.$name); + } + /** * Loads the translator configuration. * diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml new file mode 100644 index 0000000000..15dbabd5fe --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php index 0a63a29cd6..ef76796a26 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Asset; +trigger_error('The Symfony\Bundle\FrameworkBundle\Templating\Asset\PackageFactory is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Templating\Asset\PackageInterface; @@ -19,6 +21,8 @@ use Symfony\Component\Templating\Asset\PackageInterface; * Creates packages based on whether the current request is secure. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class PackageFactory { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php index 6aa8c58824..e75e25106b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php @@ -14,10 +14,14 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Asset; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Templating\Asset\PathPackage as BasePathPackage; +trigger_error('The Symfony\Bundle\FrameworkBundle\Templating\Asset\PathPackage is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * The path packages adds a version and a base path to asset URLs. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class PathPackage extends BasePathPackage { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index dce63a74b9..df30fe059e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -32,6 +32,7 @@ "doctrine/annotations": "~1.0" }, "require-dev": { + "symfony/asset": "~2.7|~3.0.0", "symfony/browser-kit": "~2.4|~3.0.0", "symfony/console": "~2.6|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index afd8f8177f..82325777bc 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.7.0 +----- + + * added support for the new Asset component (from Twig bridge) + * deprecated the assets extension (use the one from the Twig bridge instead) + 2.6.0 ----- diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 59662e8350..97b6eb68bc 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -82,5 +82,9 @@ class ExtensionPass implements CompilerPassInterface // we are on Symfony <3.0, where the setContainer method exists $container->getDefinition('twig.app_variable')->addMethodCall('setContainer', array(new Reference('service_container'))); } + + if ($container->has('assets.packages')) { + $container->getDefinition('twig.extension.new_assets')->addTag('twig.extension'); + } } } diff --git a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php index d596f97e52..4efc52b0c5 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php @@ -18,6 +18,8 @@ use Symfony\Component\Routing\RequestContext; * Twig extension for Symfony assets helper. * * @author Fabien Potencier + * + * @deprecated since 2.7, to be removed in 3.0. Use Symfony\Component\Twig\Extension\AssetExtension instead. */ class AssetsExtension extends \Twig_Extension { @@ -57,6 +59,8 @@ class AssetsExtension extends \Twig_Extension */ public function getAssetUrl($path, $packageName = null, $absolute = false, $version = null) { + trigger_error('The Twig asset() function was deprecated in 2.7 and will be removed in 3.0. Please use asset_path() instead.', E_USER_DEPRECATED); + $url = $this->container->get('templating.helper.assets')->getUrl($path, $packageName, $version); if (!$absolute) { @@ -75,6 +79,8 @@ class AssetsExtension extends \Twig_Extension */ public function getAssetsVersion($packageName = null) { + trigger_error('The Twig assets_version() function was deprecated in 2.7 and will be removed in 3.0. Please use asset_version() instead.', E_USER_DEPRECATED); + return $this->container->get('templating.helper.assets')->getVersion($packageName); } diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 8f6002d47e..64e74274ff 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -92,6 +92,10 @@ + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php similarity index 98% rename from src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php rename to src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php index eddf7adcc0..425349942f 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php @@ -15,7 +15,7 @@ use Symfony\Bundle\TwigBundle\Extension\AssetsExtension; use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Component\Routing\RequestContext; -class AssetsExtensionTest extends TestCase +class LegacyAssetsExtensionTest extends TestCase { /** * @dataProvider provideGetGetAssetUrlArguments diff --git a/src/Symfony/Component/Asset/.gitignore b/src/Symfony/Component/Asset/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/Asset/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Asset/CHANGELOG.md b/src/Symfony/Component/Asset/CHANGELOG.md new file mode 100644 index 0000000000..619a423402 --- /dev/null +++ b/src/Symfony/Component/Asset/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.7.0 +----- + + * added the component diff --git a/src/Symfony/Component/Asset/Context/ContextInterface.php b/src/Symfony/Component/Asset/Context/ContextInterface.php new file mode 100644 index 0000000000..83282021ae --- /dev/null +++ b/src/Symfony/Component/Asset/Context/ContextInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Context; + +/** + * Holds information about the current request. + * + * @author Fabien Potencier + */ +interface ContextInterface +{ + /** + * Gets the base path. + * + * @return string The base path + */ + public function getBasePath(); + + /** + * Checks whether the request is secure or not. + * + * @return bool true if the request is secure, false otherwise + */ + public function isSecure(); +} diff --git a/src/Symfony/Component/Asset/Context/RequestStackContext.php b/src/Symfony/Component/Asset/Context/RequestStackContext.php new file mode 100644 index 0000000000..ba7113851d --- /dev/null +++ b/src/Symfony/Component/Asset/Context/RequestStackContext.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Context; + +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * Uses a RequestStack to populate the context. + * + * @author Fabien Potencier + */ +class RequestStackContext implements ContextInterface +{ + private $requestStack; + + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() + { + if (!$request = $this->requestStack->getMasterRequest()) { + return ''; + } + + return $request->getBasePath(); + } + + /** + * {@inheritdoc} + */ + public function isSecure() + { + if (!$request = $this->requestStack->getMasterRequest()) { + return false; + } + + return $request->isSecure(); + } +} diff --git a/src/Symfony/Component/Asset/Exception/ExceptionInterface.php b/src/Symfony/Component/Asset/Exception/ExceptionInterface.php new file mode 100644 index 0000000000..cce1b5cced --- /dev/null +++ b/src/Symfony/Component/Asset/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Exception; + +/** + * Base ExceptionInterface for the Asset component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Asset/Exception/InvalidArgumentException.php b/src/Symfony/Component/Asset/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000..0945d8fad1 --- /dev/null +++ b/src/Symfony/Component/Asset/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Exception; + +/** + * Base InvalidArgumentException for the Asset component. + * + * @author Fabien Potencier + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Asset/Exception/LogicException.php b/src/Symfony/Component/Asset/Exception/LogicException.php new file mode 100644 index 0000000000..f291d88d52 --- /dev/null +++ b/src/Symfony/Component/Asset/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Exception; + +/** + * Base LogicException for the Asset component. + * + * @author Fabien Potencier + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Asset/LICENSE b/src/Symfony/Component/Asset/LICENSE new file mode 100644 index 0000000000..43028bc600 --- /dev/null +++ b/src/Symfony/Component/Asset/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php new file mode 100644 index 0000000000..50f813d86d --- /dev/null +++ b/src/Symfony/Component/Asset/Package.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +use Symfony\Component\Asset\Context\ContextInterface; +use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; + +/** + * Basic package that adds a version to asset URLs. + * + * @author Kris Wallsmith + * @author Fabien Potencier + */ +class Package implements PackageInterface +{ + private $versionStrategy; + private $context; + + public function __construct(VersionStrategyInterface $versionStrategy) + { + $this->versionStrategy = $versionStrategy; + } + + /** + * {@inheritdoc} + */ + public function getVersion($path) + { + return $this->versionStrategy->getVersion($path); + } + + /** + * {@inheritdoc} + */ + public function getUrl($path) + { + if ($this->isAbsoluteUrl($path)) { + return $path; + } + + return $this->versionStrategy->applyVersion($path); + } + + public function setContext(ContextInterface $context) + { + $this->context = $context; + } + + /** + * @return ContextInterface|null + */ + protected function getContext() + { + return $this->context; + } + + protected function getVersionStrategy() + { + return $this->versionStrategy; + } + + protected function isAbsoluteUrl($url) + { + return false !== strpos($url, '://') || '//' === substr($url, 0, 2); + } +} diff --git a/src/Symfony/Component/Asset/PackageInterface.php b/src/Symfony/Component/Asset/PackageInterface.php new file mode 100644 index 0000000000..b9e9ff90b9 --- /dev/null +++ b/src/Symfony/Component/Asset/PackageInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +/** + * Asset package interface. + * + * @author Kris Wallsmith + */ +interface PackageInterface +{ + /** + * Returns the asset version for an asset. + * + * @param string $path A path + * + * @return string The version string + */ + public function getVersion($path); + + /** + * Returns an absolute or root-relative public path. + * + * @param string $path A path + * + * @return string The public path + */ + public function getUrl($path); +} diff --git a/src/Symfony/Component/Asset/Packages.php b/src/Symfony/Component/Asset/Packages.php new file mode 100644 index 0000000000..e980b7608e --- /dev/null +++ b/src/Symfony/Component/Asset/Packages.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; + +/** + * Helps manage asset URLs. + * + * @author Fabien Potencier + * @author Kris Wallsmith + */ +class Packages +{ + private $defaultPackage; + private $packages = array(); + + /** + * @param PackageInterface $defaultPackage The default package + * @param PackageInterface[] $packages Additional packages indexed by name + */ + public function __construct(PackageInterface $defaultPackage = null, array $packages = array()) + { + $this->defaultPackage = $defaultPackage; + + foreach ($packages as $name => $package) { + $this->addPackage($name, $package); + } + } + + /** + * Sets the default package. + * + * @param PackageInterface $defaultPackage The default package + */ + public function setDefaultPackage(PackageInterface $defaultPackage) + { + $this->defaultPackage = $defaultPackage; + } + + /** + * Adds a package. + * + * @param string $name The package name + * @param PackageInterface $package The package + */ + public function addPackage($name, PackageInterface $package) + { + $this->packages[$name] = $package; + } + + /** + * Returns an asset package. + * + * @param string $name The name of the package or null for the default package + * + * @return PackageInterface An asset package + * + * @throws InvalidArgumentException If there is no package by that name + * @throws LogicException If no default package is defined + */ + public function getPackage($name = null) + { + if (null === $name) { + if (null === $this->defaultPackage) { + throw new LogicException('There is no default asset package, configure one first.'); + } + + return $this->defaultPackage; + } + + if (!isset($this->packages[$name])) { + throw new InvalidArgumentException(sprintf('There is no "%s" asset package.', $name)); + } + + return $this->packages[$name]; + } + + /** + * Gets the version to add to public URL. + * + * @param string $path A public path + * @param string $packageName A package name + * + * @return string The current version + */ + public function getVersion($path, $packageName = null) + { + return $this->getPackage($packageName)->getVersion($path); + } + + /** + * Returns the public path. + * + * Absolute paths (i.e. http://...) are returned unmodified. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string A public path which takes into account the base path and URL path + */ + public function getUrl($path, $packageName = null) + { + return $this->getPackage($packageName)->getUrl($path); + } +} diff --git a/src/Symfony/Component/Asset/PathPackage.php b/src/Symfony/Component/Asset/PathPackage.php new file mode 100644 index 0000000000..9f9c4ea17a --- /dev/null +++ b/src/Symfony/Component/Asset/PathPackage.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; + +/** + * Package that adds a base path to asset URLs in addition to a version. + * + * In addition to the provided base path, this package also automatically + * prepends the current request base path if a Context is available to + * allow a website to be hosted easily under any given path under the Web + * Server root directory. + * + * @author Fabien Potencier + */ +class PathPackage extends Package +{ + private $basePath; + + /** + * @param string $basePath The base path to be prepended to relative paths + * @param VersionStrategyInterface $versionStrategy The version strategy + */ + public function __construct($basePath, VersionStrategyInterface $versionStrategy) + { + parent::__construct($versionStrategy); + + if (!$basePath) { + $this->basePath = '/'; + } else { + if ('/' != $basePath[0]) { + $basePath = '/'.$basePath; + } + + $this->basePath = rtrim($basePath, '/').'/'; + } + } + + /** + * {@inheritdoc} + */ + public function getUrl($path) + { + if ($this->isAbsoluteUrl($path)) { + return $path; + } + + return $this->getBasePath().ltrim($this->getVersionStrategy()->applyVersion($path), '/'); + } + + /** + * Returns the base path. + * + * @return string The base path + */ + public function getBasePath() + { + if (null !== $context = $this->getContext()) { + return $context->getBasePath().$this->basePath; + } + + return $this->basePath; + } +} diff --git a/src/Symfony/Component/Asset/README.md b/src/Symfony/Component/Asset/README.md new file mode 100644 index 0000000000..f2e66b832e --- /dev/null +++ b/src/Symfony/Component/Asset/README.md @@ -0,0 +1,166 @@ +Asset Component +=============== + +The Asset component manages asset URLs. + +Versioned Asset URLs +-------------------- + +The basic `Package` adds a version to generated asset URLs: + +```php +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +$package = new Package(new StaticVersionStrategy('v1')); + +echo $package->getUrl('/me.png'); +// /me.png?v1 +``` + +The default format can be configured: + +```php +$package = new Package(new StaticVersionStrategy('v1', '%s?version=%s')); + +echo $package->getUrl('/me.png'); +// /me.png?version=v1 + +// put the version before the path +$package = new Package(new StaticVersionStrategy('v1', 'version-%2$s/%1$s')); + +echo $package->getUrl('/me.png'); +// /version-v1/me.png +``` + +Asset URLs Base Path +-------------------- + +When all assets are stored in a common path, use the `PathPackage` to avoid +repeating yourself: + +```php +use Symfony\Component\Asset\PathPackage; + +$package = new PathPackage('/images', new StaticVersionStrategy('v1')); + +echo $package->getUrl('/me.png'); +// /images/me.png?v1 +``` + +Asset URLs Base URLs +-------------------- + +If your assets are hosted on different domain name than the main website, use +the `UrlPackage` class: + +```php +use Symfony\Component\Asset\UrlPackage; + +$package = new UrlPackage('http://assets.example.com/images/', new StaticVersionStrategy('v1')); + +echo $package->getUrl('/me.png'); +// http://assets.example.com/images/me.png?v1 +``` + +One technique used to speed up page rendering in browsers is to use several +domains for assets; this is possible by passing more than one base URLs: + +```php +use Symfony\Component\Asset\UrlPackage; + +$urls = array( + 'http://a1.example.com/images/', + 'http://a2.example.com/images/', +); +$package = new UrlPackage($urls, new StaticVersionStrategy('v1')); + +echo $package->getUrl('/me.png'); +// http://a1.example.com/images/me.png?v1 +``` + +Note that it's also guaranteed that any given path will always use the same +base URL to be nice with HTTP caching mechanisms. + +HttpFoundation Integration +-------------------------- + +If you are using HttpFoundation for your project, set the Context to get +additional features for free: + +```php +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\Context\RequestStackContext; + +$package = new PathPackage('images', new StaticVersionStrategy('v1')); +$package->setContext(new RequestStackContext($requestStack)); + +echo $package->getUrl('/me.png'); +// /somewhere/images/me.png?v1 +``` + +In addition to the configured base path, `PathPackage` now also automatically +prepends the current request base URL to assets to allow your website to be +hosted anywhere under the web server root directory. + +```php +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\Context\RequestStackContext; + +$package = new UrlPackage(array('http://example.com/', 'https://example.com/'), new StaticVersionStrategy('v1')); +$package->setContext(new RequestStackContext($requestStack)); + +echo $package->getUrl('/me.png'); +// https://example.com/images/me.png?v1 +``` + +`UrlPackage` now uses the current request scheme (HTTP or HTTPs) to select an +appropriate base URL (HTTPs or protocol-relative URLs for HTTPs requests, any +base URL for HTTP requests). + +Named Packages +-------------- + +The `Packages` class allows to easily manages several packages in a single +project by naming packages: + +```php +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\Packages; + +// by default, just add a version to all assets +$versionStrategy = new StaticVersionStrategy('v1'); +$defaultPackage = new Asset\Package($versionStrategy); + +$namedPackages = array( + // images are hosted on another web server + 'img' => new Asset\UrlPackage('http://img.example.com/', $versionStrategy), + + // documents are stored deeply under the web root directory + // let's create a shortcut + 'doc' => new Asset\PathPackage('/somewhere/deep/for/documents', $versionStrategy), +); + +// bundle all packages to make it easy to use them +$packages = new Asset\Packages($defaultPackage, $namedPackages); + +echo $packages->getUrl('/some.css'); +// /some.css?v1 + +echo $packages->getUrl('/me.png', 'img'); +// http://img.example.com/me.png?v1 + +echo $packages->getUrl('/me.pdf', 'doc'); +// /somewhere/deep/for/documents/me.pdf?v1 +``` + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Asset/ + $ composer update + $ phpunit diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php new file mode 100644 index 0000000000..a2310d5898 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Tests; + +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; + +class PackageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getConfigs + */ + public function testGetUrl($version, $format, $path, $expected) + { + $package = new Package($version ? new StaticVersionStrategy($version, $format) : new EmptyVersionStrategy()); + $this->assertEquals($expected, $package->getUrl($path)); + } + + public function getConfigs() + { + return array( + array('v1', '', 'http://example.com/foo', 'http://example.com/foo'), + array('v1', '', 'https://example.com/foo', 'https://example.com/foo'), + array('v1', '', '//example.com/foo', '//example.com/foo'), + + array('v1', '', '/foo', '/foo?v1'), + array('v1', '', 'foo', 'foo?v1'), + + array(null, '', '/foo', '/foo'), + array(null, '', 'foo', 'foo'), + + array('v1', 'version-%2$s/%1$s', '/foo', '/version-v1/foo'), + array('v1', 'version-%2$s/%1$s', 'foo', 'version-v1/foo'), + array('v1', 'version-%2$s/%1$s', 'foo/', 'version-v1/foo/'), + array('v1', 'version-%2$s/%1$s', '/foo/', '/version-v1/foo/'), + ); + } + + public function testGetVersion() + { + $package = new Package(new StaticVersionStrategy('v1')); + $this->assertEquals('v1', $package->getVersion('/foo')); + } +} diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php new file mode 100644 index 0000000000..81db37b996 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Tests; + +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; + +class PackagesTest extends \PHPUnit_Framework_TestCase +{ + public function testGetterSetters() + { + $packages = new Packages(); + $packages->setDefaultPackage($default = $this->getMock('Symfony\Component\Asset\PackageInterface')); + $packages->addPackage('a', $a = $this->getMock('Symfony\Component\Asset\PackageInterface')); + + $this->assertEquals($default, $packages->getPackage()); + $this->assertEquals($a, $packages->getPackage('a')); + + $packages = new Packages($default, array('a' => $a)); + + $this->assertEquals($default, $packages->getPackage()); + $this->assertEquals($a, $packages->getPackage('a')); + } + + public function testGetVersion() + { + $packages = new Packages( + new Package(new StaticVersionStrategy('default')), + array('a' => new Package(new StaticVersionStrategy('a'))) + ); + + $this->assertEquals('default', $packages->getVersion('/foo')); + $this->assertEquals('a', $packages->getVersion('/foo', 'a')); + } + + public function testGetUrl() + { + $packages = new Packages( + new Package(new StaticVersionStrategy('default')), + array('a' => new Package(new StaticVersionStrategy('a'))) + ); + + $this->assertEquals('/foo?default', $packages->getUrl('/foo')); + $this->assertEquals('/foo?a', $packages->getUrl('/foo', 'a')); + } + + /** + * @expectedException LogicException + */ + public function testNoDefaultPackage() + { + $packages = new Packages(); + $packages->getPackage(); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testUndefinedPackage() + { + $packages = new Packages(); + $packages->getPackage('a'); + } +} diff --git a/src/Symfony/Component/Asset/Tests/PathPackageTest.php b/src/Symfony/Component/Asset/Tests/PathPackageTest.php new file mode 100644 index 0000000000..3378ee1dcd --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/PathPackageTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Tests; + +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +class PathPackageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getConfigs + */ + public function testGetUrl($basePath, $format, $path, $expected) + { + $package = new PathPackage($basePath, new StaticVersionStrategy('v1', $format)); + $this->assertEquals($expected, $package->getUrl($path)); + } + + public function getConfigs() + { + return array( + array('/foo', '', 'http://example.com/foo', 'http://example.com/foo'), + array('/foo', '', 'https://example.com/foo', 'https://example.com/foo'), + array('/foo', '', '//example.com/foo', '//example.com/foo'), + + array('', '', '/foo', '/foo?v1'), + + array('/foo', '', '/foo', '/foo/foo?v1'), + array('/foo', '', 'foo', '/foo/foo?v1'), + array('foo', '', 'foo', '/foo/foo?v1'), + array('foo/', '', 'foo', '/foo/foo?v1'), + array('/foo/', '', 'foo', '/foo/foo?v1'), + + array('/foo', 'version-%2$s/%1$s', '/foo', '/foo/version-v1/foo'), + array('/foo', 'version-%2$s/%1$s', 'foo', '/foo/version-v1/foo'), + array('/foo', 'version-%2$s/%1$s', 'foo/', '/foo/version-v1/foo/'), + array('/foo', 'version-%2$s/%1$s', '/foo/', '/foo/version-v1/foo/'), + ); + } + + /** + * @dataProvider getContextConfigs + */ + public function testGetUrlWithContext($basePathRequest, $basePath, $format, $path, $expected) + { + $package = new PathPackage($basePath, new StaticVersionStrategy('v1', $format)); + $package->setContext($this->getContext($basePathRequest)); + $this->assertEquals($expected, $package->getUrl($path)); + } + + public function getContextConfigs() + { + return array( + array('', '/foo', '', '/foo', '/foo/foo?v1'), + array('', '/foo', '', 'foo', '/foo/foo?v1'), + array('', 'foo', '', 'foo', '/foo/foo?v1'), + array('', 'foo/', '', 'foo', '/foo/foo?v1'), + array('', '/foo/', '', 'foo', '/foo/foo?v1'), + + array('/bar', '/foo', '', '/foo', '/bar/foo/foo?v1'), + array('/bar', '/foo', '', 'foo', '/bar/foo/foo?v1'), + array('/bar', 'foo', '', 'foo', '/bar/foo/foo?v1'), + array('/bar', 'foo/', '', 'foo', '/bar/foo/foo?v1'), + array('/bar', '/foo/', '', 'foo', '/bar/foo/foo?v1'), + ); + } + + private function getContext($basePath) + { + $context = $this->getMock('Symfony\Component\Asset\Context\ContextInterface'); + $context->expects($this->any())->method('getBasePath')->will($this->returnValue($basePath)); + + return $context; + } +} diff --git a/src/Symfony/Component/Asset/Tests/UrlPackageTest.php b/src/Symfony/Component/Asset/Tests/UrlPackageTest.php new file mode 100644 index 0000000000..7066f84686 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/UrlPackageTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Tests; + +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; + +class UrlPackageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getConfigs + */ + public function testGetUrl($baseUrls, $format, $path, $expected) + { + $package = new UrlPackage($baseUrls, new StaticVersionStrategy('v1', $format)); + $this->assertEquals($expected, $package->getUrl($path)); + } + + public function getConfigs() + { + return array( + array('http://example.net', '', 'http://example.com/foo', 'http://example.com/foo'), + array('http://example.net', '', 'https://example.com/foo', 'https://example.com/foo'), + array('http://example.net', '', '//example.com/foo', '//example.com/foo'), + + array('http://example.com', '', '/foo', 'http://example.com/foo?v1'), + array('http://example.com', '', 'foo', 'http://example.com/foo?v1'), + array('http://example.com/', '', 'foo', 'http://example.com/foo?v1'), + array('http://example.com/foo', '', 'foo', 'http://example.com/foo/foo?v1'), + array('http://example.com/foo/', '', 'foo', 'http://example.com/foo/foo?v1'), + + array(array('http://example.com'), '', '/foo', 'http://example.com/foo?v1'), + array(array('http://example.com', 'http://example.net'), '', '/foo', 'http://example.com/foo?v1'), + array(array('http://example.com', 'http://example.net'), '', '/fooa', 'http://example.net/fooa?v1'), + + array('http://example.com', 'version-%2$s/%1$s', '/foo', 'http://example.com/version-v1/foo'), + array('http://example.com', 'version-%2$s/%1$s', 'foo', 'http://example.com/version-v1/foo'), + array('http://example.com', 'version-%2$s/%1$s', 'foo/', 'http://example.com/version-v1/foo/'), + array('http://example.com', 'version-%2$s/%1$s', '/foo/', 'http://example.com/version-v1/foo/'), + ); + } + + /** + * @dataProvider getContextConfigs + */ + public function testGetUrlWithContext($secure, $baseUrls, $format, $path, $expected) + { + $package = new UrlPackage($baseUrls, new StaticVersionStrategy('v1', $format)); + $package->setContext($this->getContext($secure)); + $this->assertEquals($expected, $package->getUrl($path)); + } + + public function getContextConfigs() + { + return array( + array(false, 'http://example.com', '', 'foo', 'http://example.com/foo?v1'), + array(false, array('http://example.com'), '', 'foo', 'http://example.com/foo?v1'), + array(false, array('http://example.com', 'https://example.com'), '', 'foo', 'http://example.com/foo?v1'), + array(false, array('http://example.com', 'https://example.com'), '', 'fooa', 'https://example.com/fooa?v1'), + array(false, array('http://example.com/bar'), '', 'foo', 'http://example.com/bar/foo?v1'), + array(false, array('http://example.com/bar/'), '', 'foo', 'http://example.com/bar/foo?v1'), + array(false, array('//example.com/bar/'), '', 'foo', '//example.com/bar/foo?v1'), + + array(true, array('http://example.com'), '', 'foo', 'http://example.com/foo?v1'), + array(true, array('http://example.com', 'https://example.com'), '', 'foo', 'https://example.com/foo?v1'), + ); + } + + /** + * @expectedException LogicException + */ + public function testNoBaseUrls() + { + new UrlPackage(array(), new EmptyVersionStrategy()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testWrongBaseUrl() + { + new UrlPackage(array('not-a-url'), new EmptyVersionStrategy()); + } + + private function getContext($secure) + { + $context = $this->getMock('Symfony\Component\Asset\Context\ContextInterface'); + $context->expects($this->any())->method('isSecure')->will($this->returnValue($secure)); + + return $context; + } +} diff --git a/src/Symfony/Component/Asset/UrlPackage.php b/src/Symfony/Component/Asset/UrlPackage.php new file mode 100644 index 0000000000..d675cf0037 --- /dev/null +++ b/src/Symfony/Component/Asset/UrlPackage.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; + +/** + * Package that adds a base URL to asset URLs in addition to a version. + * + * The package allows to use more than one base URLs in which case + * it randomly chooses one for each asset; it also guarantees that + * any given path will always use the same base URL to be nice with + * HTTP caching mechanisms. + * + * When the request context is available, this package can choose the + * best base URL to use based on the current request scheme: + * + * * For HTTP request, it chooses between all base URLs; + * * For HTTPs requests, it chooses between HTTPs base URLs and relative protocol URLs + * or falls back to any base URL if no secure ones are available. + * + * @author Fabien Potencier + */ +class UrlPackage extends Package +{ + private $baseUrls = array(); + private $sslUrls; + private $sslPackage; + + /** + * @param string|array $baseUrls Base asset URLs + * @param VersionStrategyInterface $versionStrategy The version strategy + */ + public function __construct($baseUrls = array(), VersionStrategyInterface $versionStrategy) + { + parent::__construct($versionStrategy); + + if (!is_array($baseUrls)) { + $baseUrls = (array) $baseUrls; + } + + if (!$baseUrls) { + throw new LogicException('You must provide at least one base URL.'); + } + + foreach ($baseUrls as $baseUrl) { + $this->baseUrls[] = rtrim($baseUrl, '/'); + } + + $sslUrls = $this->getSslUrls($baseUrls); + + if ($sslUrls && $baseUrls !== $sslUrls) { + $this->sslPackage = new UrlPackage($sslUrls, $versionStrategy); + } + } + + /** + * {@inheritdoc} + */ + public function getUrl($path) + { + if ($this->isAbsoluteUrl($path)) { + return $path; + } + + if (null !== $this->sslPackage && ($context = $this->getContext()) && $context->isSecure()) { + return $this->sslPackage->getUrl($path); + } + + $url = $this->getVersionStrategy()->applyVersion($path); + + if ($url && '/' != $url[0]) { + $url = '/'.$url; + } + + return $this->getBaseUrl($path).$url; + } + + /** + * Returns the base URL for a path. + * + * @param string $path + * + * @return string The base URL + */ + public function getBaseUrl($path) + { + if (1 === count($this->baseUrls)) { + return $this->baseUrls[0]; + } + + return $this->baseUrls[$this->chooseBaseUrl($path)]; + } + + /** + * Determines which base URL to use for the given path. + * + * Override this method to change the default distribution strategy. + * This method should always return the same base URL for a given path. + * + * @param string $path + * + * @return string The base URL for the given path + */ + protected function chooseBaseUrl($path) + { + return fmod(hexdec(substr(hash('sha256', $path), 0, 10)), count($this->baseUrls)); + } + + private function getSslUrls($urls) + { + $sslUrls = array(); + foreach ($urls as $url) { + if ('https://' === substr($url, 0, 8) || '//' === substr($url, 0, 2)) { + $sslUrls[] = $url; + } elseif ('http://' !== substr($url, 0, 7)) { + throw new InvalidArgumentException(sprintf('"%s" is not a valid URL', $url)); + } + } + + return $sslUrls; + } +} diff --git a/src/Symfony/Component/Asset/VersionStrategy/EmptyVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/EmptyVersionStrategy.php new file mode 100644 index 0000000000..aa06eaa55e --- /dev/null +++ b/src/Symfony/Component/Asset/VersionStrategy/EmptyVersionStrategy.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\VersionStrategy; + +/** + * Disable version for all assets. + * + * @author Fabien Potencier + */ +class EmptyVersionStrategy implements VersionStrategyInterface +{ + /** + * {@inheritdoc} + */ + public function getVersion($path) + { + return ''; + } + + /** + * {@inheritdoc} + */ + public function applyVersion($path) + { + return $path; + } +} diff --git a/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php new file mode 100644 index 0000000000..6028eb57fe --- /dev/null +++ b/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\VersionStrategy; + +/** + * Returns the same version for all assets. + * + * @author Fabien Potencier + */ +class StaticVersionStrategy implements VersionStrategyInterface +{ + private $version; + private $format; + + public function __construct($version, $format = null) + { + $this->version = $version; + $this->format = $format ?: '%s?%s'; + } + + /** + * {@inheritdoc} + */ + public function getVersion($path) + { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function applyVersion($path) + { + $versionized = sprintf($this->format, ltrim($path, '/'), $this->getVersion($path)); + + if ($path && '/' == $path[0]) { + return '/'.$versionized; + } + + return $versionized; + } +} diff --git a/src/Symfony/Component/Asset/VersionStrategy/VersionStrategyInterface.php b/src/Symfony/Component/Asset/VersionStrategy/VersionStrategyInterface.php new file mode 100644 index 0000000000..a0fb260033 --- /dev/null +++ b/src/Symfony/Component/Asset/VersionStrategy/VersionStrategyInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\VersionStrategy; + +/** + * Asset version strategy interface. + * + * @author Fabien Potencier + */ +interface VersionStrategyInterface +{ + /** + * Returns the asset version for an asset. + * + * @param string $path A path + * + * @return string The version string + */ + public function getVersion($path); + + /** + * Applies version to the supplied path. + * + * @param string $path A path + * + * @return string The versionized path + */ + public function applyVersion($path); +} diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json new file mode 100644 index 0000000000..cf058bb3b2 --- /dev/null +++ b/src/Symfony/Component/Asset/composer.json @@ -0,0 +1,36 @@ +{ + "name": "symfony/asset", + "type": "library", + "description": "Symfony Asset Component", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "require-dev": { + "symfony/http-foundation": "~2.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Asset\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + } +} diff --git a/src/Symfony/Component/Asset/phpunit.xml.dist b/src/Symfony/Component/Asset/phpunit.xml.dist new file mode 100644 index 0000000000..547ecdaa7a --- /dev/null +++ b/src/Symfony/Component/Asset/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + diff --git a/src/Symfony/Component/Templating/Asset/Package.php b/src/Symfony/Component/Templating/Asset/Package.php index 02a1269bd9..cfc33ccbcf 100644 --- a/src/Symfony/Component/Templating/Asset/Package.php +++ b/src/Symfony/Component/Templating/Asset/Package.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Templating\Asset; +trigger_error('The Symfony\Component\Templating\Asset\Package is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * The basic package will add a version to asset URLs. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class Package implements PackageInterface { diff --git a/src/Symfony/Component/Templating/Asset/PackageInterface.php b/src/Symfony/Component/Templating/Asset/PackageInterface.php index 7317b555ac..f19f6fc3c4 100644 --- a/src/Symfony/Component/Templating/Asset/PackageInterface.php +++ b/src/Symfony/Component/Templating/Asset/PackageInterface.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Templating\Asset; +trigger_error('The Symfony\Component\Templating\Asset\PackageInterface is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * Asset package interface. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ interface PackageInterface { diff --git a/src/Symfony/Component/Templating/Asset/PathPackage.php b/src/Symfony/Component/Templating/Asset/PathPackage.php index 1806107f6d..48f69523df 100644 --- a/src/Symfony/Component/Templating/Asset/PathPackage.php +++ b/src/Symfony/Component/Templating/Asset/PathPackage.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Templating\Asset; +trigger_error('The Symfony\Component\Templating\Asset\PathPackage is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * The path packages adds a version and a base path to asset URLs. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class PathPackage extends Package { diff --git a/src/Symfony/Component/Templating/Asset/UrlPackage.php b/src/Symfony/Component/Templating/Asset/UrlPackage.php index 00a21670f4..2ab1d0f9d5 100644 --- a/src/Symfony/Component/Templating/Asset/UrlPackage.php +++ b/src/Symfony/Component/Templating/Asset/UrlPackage.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Templating\Asset; +trigger_error('The Symfony\Component\Templating\Asset\UrlPackage is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * The URL packages adds a version and a base URL to asset URLs. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class UrlPackage extends Package { diff --git a/src/Symfony/Component/Templating/Helper/AssetsHelper.php b/src/Symfony/Component/Templating/Helper/AssetsHelper.php index 128843ef0d..aceff61b61 100644 --- a/src/Symfony/Component/Templating/Helper/AssetsHelper.php +++ b/src/Symfony/Component/Templating/Helper/AssetsHelper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Templating\Helper; +trigger_error('The Symfony\Component\Templating\Helper\AssetsHelper is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + use Symfony\Component\Templating\Asset\PathPackage; use Symfony\Component\Templating\Asset\UrlPackage; @@ -25,6 +27,8 @@ use Symfony\Component\Templating\Asset\UrlPackage; * * @author Fabien Potencier * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class AssetsHelper extends CoreAssetsHelper { diff --git a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php index b2d06a3362..41076a1bfb 100644 --- a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php +++ b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Templating\Helper; +trigger_error('The Symfony\Component\Templating\Helper\CoreAssetsHelper is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + use Symfony\Component\Templating\Asset\PackageInterface; /** @@ -24,6 +26,8 @@ use Symfony\Component\Templating\Asset\PackageInterface; * * @author Fabien Potencier * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class CoreAssetsHelper extends Helper implements PackageInterface { diff --git a/src/Symfony/Component/Templating/Tests/Helper/AssetsHelperTest.php b/src/Symfony/Component/Templating/Tests/Helper/LegacyAssetsHelperTest.php similarity index 98% rename from src/Symfony/Component/Templating/Tests/Helper/AssetsHelperTest.php rename to src/Symfony/Component/Templating/Tests/Helper/LegacyAssetsHelperTest.php index c29c6de1a3..8ce72c3fc1 100644 --- a/src/Symfony/Component/Templating/Tests/Helper/AssetsHelperTest.php +++ b/src/Symfony/Component/Templating/Tests/Helper/LegacyAssetsHelperTest.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Templating\Tests\Helper; use Symfony\Component\Templating\Helper\AssetsHelper; -class AssetsHelperTest extends \PHPUnit_Framework_TestCase +class LegacyAssetsHelperTest extends \PHPUnit_Framework_TestCase { public function testGetVersion() { diff --git a/src/Symfony/Component/Templating/Tests/Helper/CoreAssetsHelperTest.php b/src/Symfony/Component/Templating/Tests/Helper/LegacyCoreAssetsHelperTest.php similarity index 94% rename from src/Symfony/Component/Templating/Tests/Helper/CoreAssetsHelperTest.php rename to src/Symfony/Component/Templating/Tests/Helper/LegacyCoreAssetsHelperTest.php index b681e9fab6..6fe6423b67 100644 --- a/src/Symfony/Component/Templating/Tests/Helper/CoreAssetsHelperTest.php +++ b/src/Symfony/Component/Templating/Tests/Helper/LegacyCoreAssetsHelperTest.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Templating\Tests\Helper; use Symfony\Component\Templating\Helper\CoreAssetsHelper; -class CoreAssetsHelperTest extends \PHPUnit_Framework_TestCase +class LegacyCoreAssetsHelperTest extends \PHPUnit_Framework_TestCase { protected $package;