diff --git a/autoload.php.dist b/autoload.php.dist index 9bb114b108..fea397bb8a 100644 --- a/autoload.php.dist +++ b/autoload.php.dist @@ -15,6 +15,7 @@ $loader->registerNamespaces(array( 'Doctrine\\DBAL' => __DIR__.'/vendor/doctrine-dbal/lib', 'Doctrine' => __DIR__.'/vendor/doctrine/lib', 'Zend' => __DIR__.'/vendor/zend/library', + 'Assetic' => __DIR__.'/vendor/assetic/src', )); $loader->registerPrefixes(array( 'Swift_' => __DIR__.'/vendor/swiftmailer/lib/classes', diff --git a/src/Symfony/Bundle/AsseticBundle/AsseticBundle.php b/src/Symfony/Bundle/AsseticBundle/AsseticBundle.php new file mode 100644 index 0000000000..411d639484 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/AsseticBundle.php @@ -0,0 +1,35 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle; + +use Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler\AssetManagerPass; +use Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler\FilterManagerPass; +use Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler\TemplatingPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * Assetic integration. + * + * @author Kris Wallsmith + */ +class AsseticBundle extends Bundle +{ + public function registerExtensions(ContainerBuilder $container) + { + parent::registerExtensions($container); + + $container->addCompilerPass(new AssetManagerPass()); + $container->addCompilerPass(new FilterManagerPass()); + $container->addCompilerPass(new TemplatingPass()); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/CacheWarmer/AssetWriterCacheWarmer.php b/src/Symfony/Bundle/AsseticBundle/CacheWarmer/AssetWriterCacheWarmer.php new file mode 100644 index 0000000000..6b51479086 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/CacheWarmer/AssetWriterCacheWarmer.php @@ -0,0 +1,38 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\CacheWarmer; + +use Assetic\AssetManager; +use Assetic\AssetWriter; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer; + +class AssetWriterCacheWarmer extends CacheWarmer +{ + protected $am; + protected $writer; + + public function __construct(AssetManager $am, AssetWriter $writer) + { + $this->am = $am; + $this->writer = $writer; + } + + public function warmUp($cacheDir) + { + $this->writer->writeManagerAssets($this->am); + } + + public function isOptional() + { + return false; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/CacheWarmer/PhpTemplatingAssetsCacheWarmer.php b/src/Symfony/Bundle/AsseticBundle/CacheWarmer/PhpTemplatingAssetsCacheWarmer.php new file mode 100644 index 0000000000..3d2325b8b6 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/CacheWarmer/PhpTemplatingAssetsCacheWarmer.php @@ -0,0 +1,58 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\CacheWarmer; + +use Symfony\Bundle\AsseticBundle\Templating\FormulaLoader; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer; +use Symfony\Component\HttpKernel\Kernel; + +class PhpTemplatingAssetsCacheWarmer extends CacheWarmer +{ + protected $kernel; + protected $loader; + + public function __construct(Kernel $kernel, FormulaLoader $loader) + { + $this->kernel = $kernel; + $this->loader = $loader; + } + + public function warmUp($cacheDir) + { + $formulae = array(); + foreach ($this->kernel->getBundles() as $name => $bundle) { + if (is_dir($dir = $bundle->getPath().'/Resources/views/')) { + $finder = new Finder(); + $finder->files()->name('*.php')->in($dir); + foreach ($finder as $file) { + $formulae += $this->loader->load($name.':'.substr($file->getPath(), strlen($dir)).':'.$file->getBasename()); + } + } + } + + if (is_dir($dir = $this->kernel->getRootDir().'/views/')) { + $finder = new Finder(); + $finder->files()->name('*.php')->in($dir); + foreach ($finder as $file) { + $formulae += $this->loader->load(':'.substr($file->getPath(), strlen($dir)).':'.$file->getBasename()); + } + } + + $this->writeCacheFile($cacheDir.'/assetic_php_templating_assets.php', ' + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\CacheWarmer; + +use Assetic\Extension\Twig\FormulaLoader; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer; +use Symfony\Component\HttpKernel\Kernel; + +class TwigAssetsCacheWarmer extends CacheWarmer +{ + protected $kernel; + protected $loader; + + public function __construct(Kernel $kernel, FormulaLoader $loader) + { + $this->kernel = $kernel; + $this->loader = $loader; + } + + public function warmUp($cacheDir) + { + $formulae = array(); + foreach ($this->kernel->getBundles() as $name => $bundle) { + if (is_dir($dir = $bundle->getPath().'/Resources/views/')) { + $finder = new Finder(); + $finder->files()->name('*.twig')->in($dir); + foreach ($finder as $file) { + $formulae += $this->loader->load($name.':'.substr($file->getPath(), strlen($dir)).':'.$file->getBasename()); + } + } + } + + if (is_dir($dir = $this->kernel->getRootDir().'/views/')) { + $finder = new Finder(); + $finder->files()->name('*.twig')->in($dir); + foreach ($finder as $file) { + $formulae += $this->loader->load(':'.substr($file->getPath(), strlen($dir)).':'.$file->getBasename()); + } + } + + $this->writeCacheFile($cacheDir.'/assetic_twig_assets.php', ' + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Dumps assets to the filesystem. + * + * @author Kris Wallsmith + */ +class DumpCommand extends Command +{ + protected function configure() + { + $this + ->setName('assetic:dump') + ->setDescription('Dumps all assets to the filesystem') + ->addArgument('base_dir', InputArgument::OPTIONAL, 'The base directory') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!$baseDir = $input->getArgument('base_dir')) { + $baseDir = $this->container->getParameter('assetic.document_root'); + } + + $am = $this->container->get('assetic.asset_manager'); + foreach ($am->all() as $name => $asset) { + $output->writeln('[asset] '.$name); + $asset->load(); + + $target = $baseDir . '/' . $asset->getTargetUrl(); + if (!is_dir($dir = dirname($target))) { + $output->writeln('[dir+] '.$dir); + mkdir($dir); + } + + $output->writeln('[file+] '.$asset->getTargetUrl()); + file_put_contents($target, $asset->dump()); + } + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Controller/Controller.php b/src/Symfony/Bundle/AsseticBundle/Controller/Controller.php new file mode 100644 index 0000000000..4c1da0b4f4 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Controller/Controller.php @@ -0,0 +1,60 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Controller; + +use Assetic\AssetManager; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * Serves assets. + * + * @author Kris Wallsmith + */ +class Controller +{ + protected $request; + protected $response; + protected $am; + + public function __construct(Request $request, Response $response, AssetManager $am) + { + $this->request = $request; + $this->response = $response; + $this->am = $am; + } + + public function render($name) + { + if (!$this->am->has($name)) { + throw new NotFoundHttpException('Asset Not Found'); + } + + $asset = $this->am->get($name); + + // validate if-modified-since + if (null !== $lastModified = $asset->getLastModified()) { + $date = new \DateTime(); + $date->setTimestamp($lastModified); + $this->response->setLastModified($date); + + if ($this->response->isNotModified($this->request)) { + return $this->response; + } + } + + $this->response->setContent($asset->dump()); + + return $this->response; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/DependencyInjection/AsseticExtension.php b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/AsseticExtension.php new file mode 100644 index 0000000000..65307485ce --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/AsseticExtension.php @@ -0,0 +1,131 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\FileLocator; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; + +/** + * Semantic asset configuration. + * + * @author Kris Wallsmith + */ +class AsseticExtension extends Extension +{ + /** + * Loads the configuration. + * + * When the debug flag is true, files in an asset collections will be + * rendered individually. + * + * In XML: + * + * + * + * In YAML: + * + * assetic: + * debug: true + * use_controller: true + * document_root: /path/to/document/root + * closure: /path/to/google_closure/compiler.jar + * yui: /path/to/yuicompressor.jar + * default_javascripts_output: js/build/*.js + * default_stylesheets_output: css/build/*.css + * + * @param array $configs An array of configuration settings + * @param ContainerBuilder $container A ContainerBuilder instance + */ + public function configLoad($configs, ContainerBuilder $container) + { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('assetic.xml'); + $loader->load('templating_twig.xml'); + // $loader->load('templating_php.xml'); // not ready yet + + foreach (self::normalizeKeys($configs) as $config) { + if (isset($config['debug'])) { + $container->setParameter('assetic.debug', $config['debug']); + } + + if (isset($config['use_controller'])) { + $container->setParameter('assetic.use_controller', $config['use_controller']); + } + + if (isset($config['document_root'])) { + $container->setParameter('assetic.document_root', $config['document_root']); + } + + if (isset($config['closure'])) { + $container->setParameter('assetic.google_closure_compiler_jar', $config['closure']); + } + + if (isset($config['yui'])) { + $container->setParameter('assetic.yui_jar', $config['yui']); + } + + if (isset($config['default_javascripts_output'])) { + $container->setParameter('assetic.default_javascripts_output', $config['default_javascripts_output']); + } + + if (isset($config['default_stylesheets_output'])) { + $container->setParameter('assetic.default_stylesheets_output', $config['default_stylesheets_output']); + } + } + + if ($container->getParameterBag()->resolveValue($container->getParameterBag()->get('assetic.use_controller'))) { + $loader->load('controller.xml'); + $container->setParameter('assetic.twig_extension.class', '%assetic.twig_extension.dynamic.class%'); + } else { + $loader->load('asset_writer.xml'); + $container->setParameter('assetic.twig_extension.class', '%assetic.twig_extension.static.class%'); + } + + if ($container->hasParameter('assetic.google_closure_compiler_jar')) { + $loader->load('google_closure_compiler.xml'); + } + + if ($container->hasParameter('assetic.yui_jar')) { + $loader->load('yui_compressor.xml'); + } + } + + /** + * Returns the base path for the XSD files. + * + * @return string The XSD base path + */ + public function getXsdValidationBasePath() + { + return __DIR__ . '/../Resources/config/schema'; + } + + public function getNamespace() + { + return 'http://www.symfony-project.org/schema/dic/assetic'; + } + + public function getAlias() + { + return 'assetic'; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/AssetManagerPass.php b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/AssetManagerPass.php new file mode 100644 index 0000000000..1f2c181929 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/AssetManagerPass.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged as assets to the asset manager. + * + * @author Kris Wallsmith + */ +class AssetManagerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('assetic.asset_manager')) { + return; + } + + $am = $container->getDefinition('assetic.asset_manager'); + foreach ($container->findTaggedServiceIds('assetic.asset') as $id => $attributes) { + foreach ($attributes as $attr) { + if (isset($attr['alias'])) { + $am->addMethodCall('set', array($attr['alias'], new Reference($id))); + } + } + } + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/FilterManagerPass.php b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/FilterManagerPass.php new file mode 100644 index 0000000000..7d1e1807fb --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/FilterManagerPass.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services tagged as filters to the filter manager. + * + * @author Kris Wallsmith + */ +class FilterManagerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('assetic.filter_manager')) { + return; + } + + $fm = $container->getDefinition('assetic.filter_manager'); + foreach ($container->findTaggedServiceIds('assetic.filter') as $id => $attributes) { + foreach ($attributes as $attr) { + if (isset($attr['alias'])) { + $fm->addMethodCall('set', array($attr['alias'], new Reference($id))); + } + } + } + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/TemplatingPass.php b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/TemplatingPass.php new file mode 100644 index 0000000000..89dee3f067 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/DependencyInjection/Compiler/TemplatingPass.php @@ -0,0 +1,44 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +class TemplatingPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('assetic.asset_manager')) { + return; + } + + $am = $container->getDefinition('assetic.asset_manager'); + $engines = $container->getParameterBag()->resolveValue($container->getParameter('templating.engines')); + + if (in_array('twig', $engines)) { + $am->addMethodCall('addCacheFile', array('%kernel.cache_dir%/assetic_twig_assets.php')); + } else { + foreach ($container->getTaggedServiceIds('assetic.templating.twig') as $id => $attr) { + $container->remove($id); + } + } + + if (in_array('php', $engines)) { + // $am->addMethodCall('addCacheFile', array('%kernel.cache_dir%/assetic_php_assets.php')); + } else { + foreach ($container->getTaggedServiceIds('assetic.templating.php') as $id => $attr) { + $container->remove($id); + } + } + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Factory/AssetFactory.php b/src/Symfony/Bundle/AsseticBundle/Factory/AssetFactory.php new file mode 100644 index 0000000000..9bf0567b7e --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Factory/AssetFactory.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Factory; + +use Assetic\Factory\AssetFactory as BaseAssetFactory; +use Symfony\Component\HttpKernel\Kernel; + +/** + * Loads asset formulae from the filesystem. + * + * @author Kris Wallsmith + */ +class AssetFactory extends BaseAssetFactory +{ + protected $kernel; + + public function __construct(Kernel $kernel, $baseDir, $debug = false) + { + $this->kernel = $kernel; + + parent::__construct($baseDir, $debug); + } + + protected function parseAsset($sourceUrl) + { + // expand bundle notation + if ('@' == $sourceUrl[0] && false !== strpos($sourceUrl, '/')) { + $sourceUrl = $this->kernel->locateResource($sourceUrl); + } + + return parent::parseAsset($sourceUrl); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Factory/CachedAssetManager.php b/src/Symfony/Bundle/AsseticBundle/Factory/CachedAssetManager.php new file mode 100644 index 0000000000..d95d92e2dd --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Factory/CachedAssetManager.php @@ -0,0 +1,90 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Factory; + +use Assetic\Factory\LazyAssetManager; + +/** + * Loads asset formulae from the filesystem. + * + * @author Kris Wallsmith + */ +class CachedAssetManager extends LazyAssetManager +{ + protected $cacheFiles = array(); + protected $fresh = true; + + /** + * Adds a cache file. + * + * Files added will be lazily loaded once needed. + * + * @param string $file A file that returns an array of formulae + */ + public function addCacheFile($file) + { + $this->cacheFiles[] = $file; + $this->fresh = false; + } + + public function getFormulae() + { + if (!$this->fresh) { + $this->loadCacheFiles(); + } + + return $this->formulae; + } + + public function get($name) + { + if (!$this->fresh) { + $this->loadCacheFiles(); + } + + return parent::get($name); + } + + public function has($name) + { + if (!$this->fresh) { + $this->loadCacheFiles(); + } + + return parent::has($name); + } + + public function all() + { + if (!$this->fresh) { + $this->loadCacheFiles(); + } + + return parent::all(); + } + + /** + * Loads formulae from the cache files. + */ + protected function loadCacheFiles() + { + while ($file = array_shift($this->cacheFiles)) { + if (!file_exists($file)) { + throw new \RuntimeException('The asset manager cache has not been warmed.'); + } + + $this->addFormulae(require $file); + } + + $this->fresh = true; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/asset_writer.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/asset_writer.xml new file mode 100644 index 0000000000..5cecd9dee2 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/asset_writer.xml @@ -0,0 +1,22 @@ + + + + + + Symfony\Bundle\AsseticBundle\CacheWarmer\AssetWriterCacheWarmer + Assetic\AssetWriter + + + + + + + + + + %assetic.document_root% + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/assetic.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/assetic.xml new file mode 100644 index 0000000000..b93a958344 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/assetic.xml @@ -0,0 +1,82 @@ + + + + + + Symfony\Bundle\AsseticBundle\Factory\AssetFactory + Symfony\Bundle\AsseticBundle\Factory\CachedAssetManager + Assetic\FilterManager + + Assetic\Filter\CoffeeScriptFilter + Assetic\Filter\CssRewriteFilter + Assetic\Filter\GoogleClosure\CompilerApiFilter + Assetic\Filter\GoogleClosure\CompilerJarFilter + Assetic\Filter\LessFilter + Assetic\Filter\Sass\SassFilter + Assetic\Filter\Sass\ScssFilter + Assetic\Filter\SprocketsFilter + + %kernel.debug% + %kernel.debug% + %kernel.root_dir%/../web + js/*.js + css/*.css + + /usr/bin/java + /usr/bin/sass + /usr/bin/lessc + /usr/bin/sprocketize + /usr/bin/coffee + + + + + + + + + + + %assetic.document_root% + %assetic.debug% + + + + + + + + + + + %assetic.lessc_bin% + + + + %assetic.sass_bin% + + + + %assetic.sass_bin% + + + + + + + %assetic.document_root% + %assetic.sprocketize_bin% + + + + %assetic.coffee_bin% + + + + + PHP/CodeSniffer.php + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/controller.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/controller.xml new file mode 100644 index 0000000000..e4b8e7b4c2 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/controller.xml @@ -0,0 +1,23 @@ + + + + + + Symfony\Bundle\AsseticBundle\Controller\Controller + Symfony\Bundle\AsseticBundle\Routing\AsseticLoader + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/google_closure_compiler.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/google_closure_compiler.xml new file mode 100644 index 0000000000..50a05e90b1 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/google_closure_compiler.xml @@ -0,0 +1,12 @@ + + + + + + + %assetic.google_closure_compiler_jar% + %assetic.java_bin% + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/schema/assetic-1.0.xsd b/src/Symfony/Bundle/AsseticBundle/Resources/config/schema/assetic-1.0.xsd new file mode 100644 index 0000000000..f3b7bc5601 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/schema/assetic-1.0.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_php.xml new file mode 100644 index 0000000000..c84cac30fa --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_php.xml @@ -0,0 +1,34 @@ + + + + + + Symfony\Bundle\AsseticBundle\Templating\AsseticHelper + Symfony\Bundle\AsseticBundle\CacheWarmer\PhpTemplatingAssetsCacheWarmer + Symfony\Bundle\AsseticBundle\Templating\FormulaLoader + + + + + + + + %assetic.debug% + %assetic.default_javascripts_output% + %assetic.default_stylesheets_output% + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_twig.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_twig.xml new file mode 100644 index 0000000000..c2b60f5f10 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/templating_twig.xml @@ -0,0 +1,34 @@ + + + + + + Symfony\Bundle\AsseticBundle\Twig\DynamicExtension + Symfony\Bundle\AsseticBundle\Twig\StaticExtension + Assetic\Extension\Twig\FormulaLoader + Symfony\Bundle\AsseticBundle\CacheWarmer\TwigAssetsCacheWarmer + + + + + + + + %assetic.debug% + %assetic.default_javascripts_output% + %assetic.default_stylesheets_output% + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Resources/config/yui_compressor.xml b/src/Symfony/Bundle/AsseticBundle/Resources/config/yui_compressor.xml new file mode 100644 index 0000000000..fd924d7193 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Resources/config/yui_compressor.xml @@ -0,0 +1,25 @@ + + + + + + Assetic\Filter\Yui\CssCompressorFilter + Assetic\Filter\Yui\JsCompressorFilter + %assetic.java_bin% + + + + + + %assetic.yui_jar% + %assetic.java_bin% + + + + %assetic.yui_jar% + %assetic.yui_java_bin% + + + diff --git a/src/Symfony/Bundle/AsseticBundle/Routing/AsseticLoader.php b/src/Symfony/Bundle/AsseticBundle/Routing/AsseticLoader.php new file mode 100644 index 0000000000..df7896e1be --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Routing/AsseticLoader.php @@ -0,0 +1,68 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Routing; + +use Assetic\AssetManager; +use Symfony\Component\Routing\Loader\Loader; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Loads routes for all assets. + * + * Assets should only be served through the routing system for ease-of-use + * during development. + * + * For example, add the following to your application's routing_dev.yml: + * + * _assetic: + * resource: . + * type: assetic + * + * In a production environment you should use the `assetic:dump` command to + * create static asset files. + * + * @author Kris Wallsmith + */ +class AsseticLoader extends Loader +{ + protected $am; + + public function __construct(AssetManager $am) + { + $this->am = $am; + } + + public function load($resource, $type = null) + { + $routes = new RouteCollection(); + foreach ($this->am->all() as $name => $asset) { + $defaults = array( + '_controller' => 'assetic.controller:render', + 'name' => $name, + ); + + if ($extension = pathinfo($asset->getTargetUrl(), PATHINFO_EXTENSION)) { + $defaults['_format'] = $extension; + } + + $routes->add('assetic_'.$name, new Route($asset->getTargetUrl(), $defaults)); + } + + return $routes; + } + + public function supports($resource, $type = null) + { + return 'assetic' == $type; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Templating/AsseticHelper.php b/src/Symfony/Bundle/AsseticBundle/Templating/AsseticHelper.php new file mode 100644 index 0000000000..40a4fbcfcc --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Templating/AsseticHelper.php @@ -0,0 +1,143 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Templating; + +use Assetic\Factory\AssetFactory; +use Symfony\Component\Templating\Helper\Helper; + +/** + * The "assetic" templating helper. + * + * @author Kris Wallsmith + */ +class AsseticHelper extends Helper +{ + protected $factory; + protected $debug; + protected $defaultJavascriptsOutput; + protected $defaultStylesheetsOutput; + + /** + * Constructor. + * + * @param AssetFactory $factory The asset factory + * @param Boolean $debug The debug mode + * @param string $defaultJavascriptsOutput The default {@link javascripts()} output string + * @param string $defaultStylesheetsOutput The default {@link stylesheets()} output string + */ + public function __construct(AssetFactory $factory, $debug = false, $defaultJavascriptsOutput = 'js/*.js', $defaultStylesheetsOutput = 'css/*.css') + { + $this->factory = $factory; + $this->debug = $debug; + $this->defaultJavascriptsOutput = $defaultJavascriptsOutput; + $this->defaultStylesheetsOutput = $defaultStylesheetsOutput; + } + + /** + * Gets the URLs for the configured asset. + * + * Usage looks something like this: + * + * assets('@jquery, js/src/core/*', '?yui_js') as $url): ?> + * + * + * + * When in debug mode, the helper returns an array of one or more URLs. + * When not in debug mode it returns an array of one URL. + * + * @param array|string $inputs An array or comma-separated list of input strings + * @param array|string $filters An array or comma-separated list of filter names + * @param array $options An array of options + * + * @return array An array of URLs for the asset + */ + public function assets($inputs = array(), $filters = array(), array $options = array()) + { + $explode = function($value) + { + return array_map('trim', explode(',', $value)); + }; + + if (!is_array($inputs)) { + $inputs = $explode($inputs); + } + + if (!is_array($filters)) { + $filters = $explode($filters); + } + + if (!isset($options['debug'])) { + $options['debug'] = $this->debug; + } + + $coll = $this->factory->createAsset($inputs, $filters, $options); + + if (!$options['debug']) { + return array($coll->getTargetUrl()); + } + + // create a pattern for each leaf's target url + $pattern = $coll->getTargetUrl(); + if (false !== $pos = strrpos($pattern, '.')) { + $pattern = substr($pattern, 0, $pos).'_*'.substr($pattern, $pos); + } else { + $pattern .= '_*'; + } + + $urls = array(); + foreach ($coll as $leaf) { + $asset = $this->factory->createAsset($leaf->getSourceUrl(), $filters, array( + 'output' => $pattern, + 'name' => 'part'.(count($urls) + 1), + 'debug' => $options['debug'], + )); + $urls[] = $asset->getTargetUrl(); + } + + return $urls; + } + + /** + * Returns an array of javascript urls. + * + * This convenience method wraps {@link assets()} and provides a default + * output string. + */ + public function javascripts($inputs = array(), $filters = array(), array $options = array()) + { + if (!isset($options['output'])) { + $options['output'] = $this->defaultJavascriptsOutput; + } + + return $this->assets($inputs, $filters, $options); + } + + /** + * Returns an array of stylesheet urls. + * + * This convenience method wraps {@link assets()} and provides a default + * output string. + */ + public function stylesheets($inputs = array(), $filters = array(), array $options = array()) + { + if (!isset($options['output'])) { + $options['output'] = $this->defaultStylesheetsOutput; + } + + return $this->assets($inputs, $filters, $options); + } + + public function getName() + { + return 'assetic'; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Templating/FormulaLoader.php b/src/Symfony/Bundle/AsseticBundle/Templating/FormulaLoader.php new file mode 100644 index 0000000000..2c55989dd3 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Templating/FormulaLoader.php @@ -0,0 +1,57 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Templating; + +use Symfony\Component\Templating\Loader\LoaderInterface; +use Symfony\Component\Templating\TemplateNameParser; + +/** + * Loads formulae from PHP templates. + * + * @author Kris Wallsmith + */ +class FormulaLoader +{ + protected $parser; + protected $loader; + + public function __construct(TemplateNameParser $parser, LoaderInterface $loader) + { + $this->parser = $parser; + $this->loader = $loader; + } + + public function load($templateName) + { + if (!$template = $this->loader->load($this->parser->parse($templateName))) { + return array(); + } + + $tokens = token_get_all($template->getContent()); + + /** + * @todo Find and extract asset formulae from calls to the following: + * + * * $view['assetic']->assets(...) + * * $view['assetic']->javascripts(...) + * * $view['assetic']->stylesheets(...) + * * $view->get('assetic')->assets(...) + * * $view->get('assetic')->javascripts(...) + * * $view->get('assetic')->stylesheets(...) + * + * The loader will also need to be aware of debug mode and the default + * output strings associated with each method. + */ + + return array(); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/TwigAssetsCacheWarmerTest.php b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/TwigAssetsCacheWarmerTest.php new file mode 100644 index 0000000000..67da110ef1 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/TwigAssetsCacheWarmerTest.php @@ -0,0 +1,69 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests\CacheWarmer; + +use Symfony\Bundle\AsseticBundle\CacheWarmer\TwigAssetsCacheWarmer; + +class TwigAssetsCacheWarmerTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Assetic\\AssetManager')) { + $this->markTestSkipped('Assetic is not available.'); + } + + $this->kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel') + ->disableOriginalConstructor() + ->setMethods(array('registerRootDir', 'registerBundles', 'registerContainerConfiguration', 'getBundles')) + ->getMock(); + $this->loader = $this->getMockBuilder('Assetic\\Extension\\Twig\\FormulaLoader') + ->disableOriginalConstructor() + ->getMock(); + + $this->cacheWarmer = new TwigAssetsCacheWarmer($this->kernel, $this->loader); + + // cache dir + $this->cacheDir = sys_get_temp_dir(); + @unlink($this->cacheDir.'/twig_assets.php'); + } + + protected function tearDown() + { + @unlink($this->cacheDir.'/twig_assets.php'); + } + + public function testCacheWarmer() + { + $bundle = $this->getMock('Symfony\\Component\\HttpKernel\\Bundle\\BundleInterface'); + + $this->kernel->expects($this->once()) + ->method('getBundles') + ->will($this->returnValue(array('MyBundle' => $bundle))); + $bundle->expects($this->once()) + ->method('getPath') + ->will($this->returnValue(strtr(__DIR__.'/bundle', '\\', '/'))); + $this->loader->expects($this->at(0)) + ->method('load') + ->with('MyBundle::grandparent.html.twig') + ->will($this->returnValue(array('grandparent' => array()))); + $this->loader->expects($this->at(1)) + ->method('load') + ->with('MyBundle:Parents/Children:child.html.twig') + ->will($this->returnValue(array('child' => array()))); + $this->loader->expects($this->at(2)) + ->method('load') + ->with('MyBundle:Parents:parent.html.twig') + ->will($this->returnValue(array('parent' => array()))); + + $this->cacheWarmer->warmUp($this->cacheDir); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/Children/child.html.twig b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/Children/child.html.twig new file mode 100644 index 0000000000..c87fa99172 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/Children/child.html.twig @@ -0,0 +1 @@ +{# empty #} \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/parent.html.twig b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/parent.html.twig new file mode 100644 index 0000000000..c87fa99172 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/Parents/parent.html.twig @@ -0,0 +1 @@ +{# empty #} \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/grandparent.html.twig b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/grandparent.html.twig new file mode 100644 index 0000000000..c87fa99172 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/CacheWarmer/bundle/Resources/views/grandparent.html.twig @@ -0,0 +1 @@ +{# empty #} \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/CachedAssetManagerTest.php b/src/Symfony/Bundle/AsseticBundle/Tests/CachedAssetManagerTest.php new file mode 100644 index 0000000000..4209ad6210 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/CachedAssetManagerTest.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests; + +use Symfony\Bundle\AsseticBundle\Factory\CachedAssetManager; + +class CachedAssetManagerTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Assetic\\AssetManager')) { + $this->markTestSkipped('Assetic is not available.'); + } + } + + public function testLoadFormulae() + { + $file = tempnam(sys_get_temp_dir(), 'assetic'); + file_put_contents($file, ' array());'); + + $factory = $this->getMockBuilder('Assetic\\Factory\\AssetFactory') + ->disableOriginalConstructor() + ->getMock(); + + $am = new CachedAssetManager($factory); + $am->addCacheFile($file); + + $this->assertTrue($am->has('foo'), '->loadFormulae() loads formulae'); + + unlink($file); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/DependencyInjection/AsseticExtensionTest.php b/src/Symfony/Bundle/AsseticBundle/Tests/DependencyInjection/AsseticExtensionTest.php new file mode 100644 index 0000000000..58a50d07f4 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/DependencyInjection/AsseticExtensionTest.php @@ -0,0 +1,154 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests\DependencyInjection; + +use Symfony\Bundle\AsseticBundle\DependencyInjection\AsseticExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; +use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\HttpFoundation\Request; + +class AsseticExtensionTest extends \PHPUnit_Framework_TestCase +{ + private $kernel; + private $container; + + protected function setUp() + { + if (!class_exists('Assetic\\AssetManager')) { + $this->markTestSkipped('Assetic is not available.'); + } + + $this->kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel') + ->disableOriginalConstructor() + ->getMock(); + + $this->container = new ContainerBuilder(); + $this->container->addScope(new Scope('request')); + $this->container->register('request', 'Symfony\\Component\\HttpFoundation\\Request')->setScope('request'); + $this->container->register('response', 'Symfony\\Component\\HttpFoundation\\Response')->setScope('prototype'); + $this->container->register('twig', 'Twig_Environment'); + $this->container->setParameter('kernel.debug', false); + $this->container->setParameter('kernel.root_dir', __DIR__); + $this->container->setParameter('kernel.bundles', array()); + } + + /** + * @dataProvider getDebugModes + */ + public function testDefaultConfig($debug) + { + $this->container->setParameter('kernel.debug', $debug); + + $extension = new AsseticExtension(); + $extension->configLoad(array(), $this->container); + + $this->assertFalse($this->container->has('assetic.filter.yui_css'), '->configLoad() does not load the yui_css filter when a yui value is not provided'); + $this->assertFalse($this->container->has('assetic.filter.yui_js'), '->configLoad() does not load the yui_js filter when a yui value is not provided'); + + // sanity check + $container = $this->getDumpedContainer(); + foreach ($container->getServiceIds() as $id) { + $container->get($id); + } + } + + public function getDebugModes() + { + return array( + array(true), + array(false), + ); + } + + public function testYuiConfig() + { + $extension = new AsseticExtension(); + $extension->configLoad(array(array('yui' => '/path/to/yuicompressor.jar')), $this->container); + + $this->assertTrue($this->container->has('assetic.filter.yui_css'), '->configLoad() loads the yui_css filter when a yui value is provided'); + $this->assertTrue($this->container->has('assetic.filter.yui_js'), '->configLoad() loads the yui_js filter when a yui value is provided'); + + // sanity check + $container = $this->getDumpedContainer(); + foreach ($container->getServiceIds() as $id) { + $container->get($id); + } + } + + /** + * @dataProvider getDocumentRootKeys + */ + public function testDocumentRoot($key) + { + $extension = new AsseticExtension(); + $extension->configLoad(array(array($key => '/path/to/web')), $this->container); + + $this->assertEquals('/path/to/web', $this->container->getParameter('assetic.document_root'), '"'.$key.'" sets document root'); + } + + public function getDocumentRootKeys() + { + return array( + array('document_root'), + array('document-root'), + ); + } + + /** + * @dataProvider getUseControllerKeys + */ + public function testUseController($bool, $includes, $omits) + { + $extension = new AsseticExtension(); + $extension->configLoad(array(array('use_controller' => $bool)), $this->container); + + foreach ($includes as $id) { + $this->assertTrue($this->container->has($id), '"'.$id.'" is registered when use_controller is '.$bool); + } + + foreach ($omits as $id) { + $this->assertFalse($this->container->has($id), '"'.$id.'" is not registered when use_controller is '.$bool); + } + + // sanity check + $container = $this->getDumpedContainer(); + foreach ($container->getServiceIds() as $id) { + $container->get($id); + } + } + + public function getUseControllerKeys() + { + return array( + array(true, array('assetic.routing_loader', 'assetic.controller'), array('assetic.asset_writer_cache_warmer', 'assetic.asset_writer')), + array(false, array('assetic.asset_writer_cache_warmer', 'assetic.asset_writer'), array('assetic.routing_loader', 'assetic.controller')), + ); + } + + private function getDumpedContainer() + { + static $i = 0; + $class = 'AsseticExtensionTestContainer'.$i++; + + $this->container->compile(); + + $dumper = new PhpDumper($this->container); + eval('?>'.$dumper->dump(array('class' => $class))); + + $container = new $class(); + $container->enterScope('request'); + $container->set('kernel', $this->kernel); + + return $container; + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/FunctionalTest.php b/src/Symfony/Bundle/AsseticBundle/Tests/FunctionalTest.php new file mode 100644 index 0000000000..67fb26aae4 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/FunctionalTest.php @@ -0,0 +1,117 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests; + +use Symfony\Bundle\AsseticBundle\Tests\Kernel\TestKernel; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpFoundation\Request; + +class FunctionalTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Assetic\\AssetManager')) { + $this->markTestSkipped('Assetic is not available.'); + } + + $cache = __DIR__.'/Kernel/cache'; + if (!is_dir($cache)) { + mkdir($cache); + } else { + shell_exec('rm -rf '.escapeshellarg(__DIR__.'/Kernel/cache/*')); + } + } + + protected function tearDown() + { + shell_exec('rm -rf '.escapeshellarg(__DIR__.'/Kernel/cache')); + } + + /** + * @dataProvider provideDebugAndAssetCount + */ + public function testKernel($debug, $count) + { + $kernel = new TestKernel('test', $debug); + $kernel->boot(); + $container = $kernel->getContainer(); + $container->get('cache_warmer')->warmUp($container->getParameter('kernel.cache_dir')); + + $assets = $container->get('assetic.asset_manager')->all(); + + $this->assertEquals($count, count($assets)); + } + + /** + * @dataProvider provideDebugAndAssetCount + */ + public function testRoutes($debug, $count) + { + $kernel = new TestKernel('test', $debug); + $kernel->boot(); + $container = $kernel->getContainer(); + $container->get('cache_warmer')->warmUp($container->getParameter('kernel.cache_dir')); + + $routes = $container->get('router')->getRouteCollection()->all(); + + $matches = 0; + foreach (array_keys($routes) as $name) { + if (0 === strpos($name, 'assetic_')) { + ++$matches; + } + } + + $this->assertEquals($count, $matches); + } + + public function testTwigRenderDebug() + { + $kernel = new TestKernel('test', true); + $kernel->boot(); + $container = $kernel->getContainer(); + $container->enterScope('request'); + $container->set('request', new Request()); + $container->get('cache_warmer')->warmUp($container->getParameter('kernel.cache_dir')); + + $content = $container->get('templating')->render('::layout.html.twig'); + $crawler = new Crawler($content); + + $this->assertEquals(2, count($crawler->filter('link[href$=".css"]'))); + $this->assertEquals(2, count($crawler->filter('script[src$=".js"]'))); + } + + public function testPhpRenderDebug() + { + $this->markTestIncomplete('PHP templating is not ready yet.'); + + $kernel = new TestKernel('test', true); + $kernel->boot(); + $container = $kernel->getContainer(); + $container->enterScope('request'); + $container->set('request', new Request()); + $container->get('cache_warmer')->warmUp($container->getParameter('kernel.cache_dir')); + + $content = $container->get('templating')->render('::layout.html.php'); + $crawler = new Crawler($content); + + $this->assertEquals(2, count($crawler->filter('link[href$=".css"]'))); + $this->assertEquals(2, count($crawler->filter('script[src$=".js"]'))); + } + + public function provideDebugAndAssetCount() + { + return array( + array(true, 4), + array(false, 2), + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/TestKernel.php b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/TestKernel.php new file mode 100644 index 0000000000..1156449f76 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/TestKernel.php @@ -0,0 +1,37 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests\Kernel; + +use Symfony\Component\DependencyInjection\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; + +class TestKernel extends Kernel +{ + public function registerRootDir() + { + return __DIR__; + } + + public function registerBundles() + { + return array( + new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new \Symfony\Bundle\TwigBundle\TwigBundle(), + new \Symfony\Bundle\AsseticBundle\AsseticBundle(), + ); + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__.'/config/config.yml'); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/config.yml b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/config.yml new file mode 100644 index 0000000000..a995bcd24d --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/config.yml @@ -0,0 +1,21 @@ +app.config: + charset: UTF-8 + error_handler: null + csrf_protection: + enabled: true + secret: xxxxxxxxxx + router: { resource: "%kernel.root_dir%/config/routing.yml" } + validation: { enabled: true, annotations: true } + templating: { engines: ['twig', 'php'] } + session: + default_locale: en + lifetime: 3600 + auto_start: false + +twig.config: + debug: %kernel.debug% + strict_variables: %kernel.debug% + +assetic.config: + use_controller: true + document_root: "%kernel.root_dir%/web" diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/routing.yml b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/routing.yml new file mode 100644 index 0000000000..1d36e9bc83 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/config/routing.yml @@ -0,0 +1,3 @@ +_assetic: + resource: . + type: assetic diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.php b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.php new file mode 100644 index 0000000000..69a886c9d3 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.php @@ -0,0 +1,11 @@ + + + + <?php $view['slots']->output('title') ?> + output('stylesheets') ?> + + + output('_content') ?> + output('javascripts') ?> + + diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.twig b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.twig new file mode 100644 index 0000000000..f2d8de83b9 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/base.html.twig @@ -0,0 +1,11 @@ + + + + {% block title '' %} + {% block stylesheets '' %} + + + {% block content '' %} + {% block javascripts '' %} + + diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.php b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.php new file mode 100644 index 0000000000..a9d15f6272 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.php @@ -0,0 +1,13 @@ +extend('::base.html.php') ?> + +start('stylesheets') ?> + stylesheets('stylesheet1.css, stylesheet2.css') as $url): ?> + + +stop() ?> + +start('javascripts') ?> + javascripts('javascript1.js, javascript2.js') as $url): ?> + + +stop() ?> diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.twig b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.twig new file mode 100644 index 0000000000..3ddf215d28 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/views/layout.html.twig @@ -0,0 +1,13 @@ +{% extends '::base.html.twig' %} + +{% block stylesheets %} + {% stylesheets 'stylesheet1.css' 'stylesheet2.css' %} + + {% endstylesheets %} +{% endblock %} + +{% block javascripts %} + {% javascripts 'javascript1.js' 'javascript2.js' %} + + {% endjavascripts %} +{% endblock %} diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript1.js b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript1.js new file mode 100644 index 0000000000..17f5a35f85 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript1.js @@ -0,0 +1 @@ +// javascript1.js \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript2.js b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript2.js new file mode 100644 index 0000000000..1790420310 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/javascript2.js @@ -0,0 +1 @@ +// javascript2.js \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet1.css b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet1.css new file mode 100644 index 0000000000..394efa6e14 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet1.css @@ -0,0 +1 @@ +/* stylesheet1.css */ \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet2.css b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet2.css new file mode 100644 index 0000000000..fbeb02a98d --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Kernel/web/stylesheet2.css @@ -0,0 +1 @@ +/* stylesheet2.css */ \ No newline at end of file diff --git a/src/Symfony/Bundle/AsseticBundle/Tests/Templating/AsseticHelperTest.php b/src/Symfony/Bundle/AsseticBundle/Tests/Templating/AsseticHelperTest.php new file mode 100644 index 0000000000..6047feb758 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Tests/Templating/AsseticHelperTest.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Tests\Templating; + +use Assetic\Asset\AssetCollection; +use Assetic\Asset\StringAsset; +use Assetic\Factory\AssetFactory; +use Symfony\Bundle\AsseticBundle\Templating\AsseticHelper; + +class AsseticHelperTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Assetic\\AssetManager')) { + $this->markTestSkipped('Assetic is not available.'); + } + } + + /** + * @dataProvider getDebugAndCount + */ + public function testUrls($debug, $count, $message) + { + $helper = new AsseticHelper(new AssetFactory('/foo', $debug), $debug); + $urls = $helper->assets(array('js/jquery.js', 'js/jquery.plugin.js')); + + $this->assertInternalType('array', $urls, '->assets() returns an array'); + $this->assertEquals($count, count($urls), $message); + } + + public function getDebugAndCount() + { + return array( + array(false, 1, '->assets() returns one url when not in debug mode'), + array(true, 2, '->assets() returns many urls when in debug mode'), + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/DynamicExtension.php b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicExtension.php new file mode 100644 index 0000000000..5d422b1631 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicExtension.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\AsseticExtension; +use Assetic\Factory\AssetFactory; + +/** + * Assetic integration. + * + * @author Kris Wallsmith + */ +class DynamicExtension extends AsseticExtension +{ + public function getTokenParsers() + { + return array( + new DynamicTokenParser($this->factory, $this->debug), + new DynamicTokenParser($this->factory, $this->debug, $this->defaultJavascriptsOutput, 'javascripts'), + new DynamicTokenParser($this->factory, $this->debug, $this->defaultStylesheetsOutput, 'stylesheets'), + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/DynamicNode.php b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicNode.php new file mode 100644 index 0000000000..2836118cb0 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicNode.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\Node; + +/** + * The "dynamic" node uses a controller to render assets. + * + * @author Kris Wallsmith + */ +class DynamicNode extends Node +{ + /** + * Renders the asset URL using Symfony's path() function. + */ + protected function getAssetUrlNode(\Twig_NodeInterface $body) + { + return new \Twig_Node_Expression_Function( + new \Twig_Node_Expression_Name('path', $body->getLine()), + new \Twig_Node(array( + new \Twig_Node_Expression_Constant('assetic_'.$this->getAttribute('asset_name'), $body->getLine()), + )), + $body->getLine() + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/DynamicTokenParser.php b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicTokenParser.php new file mode 100644 index 0000000000..cfbc97889d --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/DynamicTokenParser.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\TokenParser; + +/** + * Parses the {% assets %} tag. + * + * @author Kris Wallsmith + */ +class DynamicTokenParser extends TokenParser +{ + static protected function createNode(\Twig_NodeInterface $body, array $sourceUrls, $targetUrl, array $filterNames, $assetName, $debug = false, $lineno = 0, $tag = null) + { + return new DynamicNode($body, $sourceUrls, $targetUrl, $filterNames, $assetName, $debug, $lineno, $tag); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/StaticExtension.php b/src/Symfony/Bundle/AsseticBundle/Twig/StaticExtension.php new file mode 100644 index 0000000000..23b2fb2967 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/StaticExtension.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\AsseticExtension; +use Assetic\Factory\AssetFactory; + +/** + * Assetic integration. + * + * @author Kris Wallsmith + */ +class StaticExtension extends AsseticExtension +{ + public function getTokenParsers() + { + return array( + new StaticTokenParser($this->factory, $this->debug), + new StaticTokenParser($this->factory, $this->debug, $this->defaultJavascriptsOutput, 'javascripts'), + new StaticTokenParser($this->factory, $this->debug, $this->defaultStylesheetsOutput, 'stylesheets'), + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/StaticNode.php b/src/Symfony/Bundle/AsseticBundle/Twig/StaticNode.php new file mode 100644 index 0000000000..443dcf91b8 --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/StaticNode.php @@ -0,0 +1,36 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\Node; + +/** + * The "static" node references a file in the web directory. + * + * @author Kris Wallsmith + */ +class StaticNode extends Node +{ + /** + * Renders the asset URL using Symfony's asset() function. + */ + protected function getAssetUrlNode(\Twig_NodeInterface $body) + { + return new \Twig_Node_Expression_Function( + new \Twig_Node_Expression_Name('asset', $body->getLine()), + new \Twig_Node(array( + new \Twig_Node_Expression_Constant($this->getAttribute('target_url')), + )), + $body->getLine() + ); + } +} diff --git a/src/Symfony/Bundle/AsseticBundle/Twig/StaticTokenParser.php b/src/Symfony/Bundle/AsseticBundle/Twig/StaticTokenParser.php new file mode 100644 index 0000000000..a5bada738c --- /dev/null +++ b/src/Symfony/Bundle/AsseticBundle/Twig/StaticTokenParser.php @@ -0,0 +1,27 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\AsseticBundle\Twig; + +use Assetic\Extension\Twig\TokenParser; + +/** + * Parses the {% assets %} tag. + * + * @author Kris Wallsmith + */ +class StaticTokenParser extends TokenParser +{ + static protected function createNode(\Twig_NodeInterface $body, array $sourceUrls, $targetUrl, array $filterNames, $assetName, $debug = false, $lineno = 0, $tag = null) + { + return new StaticNode($body, $sourceUrls, $targetUrl, $filterNames, $assetName, $debug, $lineno, $tag); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e4019b86c0..faee5952f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -359,6 +359,7 @@ class FrameworkExtension extends Extension )); } + $container->setParameter('templating.engines', $config['engines']); $engines = array_map(function($engine) { return new Reference('templating.engine.'.$engine); }, $config['engines']); // Use a deligation unless only a single engine was registered diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index d12e1fac24..5d3f852737 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -111,6 +111,8 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertEquals('templating.engine.delegating', (string) $container->getAlias('templating'), '->registerTemplatingConfiguration() configures delegating loader if multiple engines are provided'); $this->assertEquals('templating.loader.chain', (string) $container->getAlias('templating.loader'), '->registerTemplatingConfiguration() configures loader chain if multiple loaders are provided'); + + $this->assertEquals(array('php', 'twig'), $container->getParameter('templating.engines'), '->registerTemplatingConfiguration() sets a templating.engines parameter'); } public function testTranslator() diff --git a/vendors.sh b/vendors.sh index dacdc692d9..13684aef1f 100755 --- a/vendors.sh +++ b/vendors.sh @@ -26,6 +26,9 @@ install_git() fi } +# Assetic +install_git assetic git://github.com/kriswallsmith/assetic.git + # Doctrine ORM install_git doctrine git://github.com/doctrine/doctrine2.git