From c163f6c4ce08a542964d1f0bd662d72ee019d826 Mon Sep 17 00:00:00 2001 From: Nicolas Le Goff Date: Tue, 9 Jul 2013 18:28:13 +0200 Subject: [PATCH] Implement bootstrap variable homepage customization through plugin system --- lib/Alchemy/Phrasea/Application.php | 4 ++ .../Command/Developer/LessCompiler.php | 44 ++++-------- .../Command/Plugin/AbstractPluginCommand.php | 9 +++ .../Provider/LessBuilderServiceProvider.php | 30 ++++++++ .../Provider/LessCompilerServiceProvider.php | 30 ++++++++ .../Plugin/Management/AutoloaderGenerator.php | 44 +++++++++++- .../Utilities/Compiler/RecessLessCompiler.php | 59 --------------- .../Phrasea/Utilities/Less/Builder.php | 71 +++++++++++++++++++ .../Phrasea/Utilities/Less/Compiler.php | 69 ++++++++++++++++++ .../Phrasea/Utilities/Less/RecessDriver.php | 36 ++++++++++ .../LessBuilderServiceProviderTest.php | 16 +++++ .../LessCompilerServiceProviderTest.php | 16 +++++ .../PluginDir/TestPlugin/account.less | 1 + .../Fixtures/PluginDir/TestPlugin/login.less | 1 + .../TestPlugin/account.less | 1 + .../PluginDirInstalled/TestPlugin/login.less | 1 + .../Management/AutoloaderGeneratorTest.php | 5 ++ .../Phrasea/Utilities/Less/BuilderTest.php | 34 +++++++++ .../Phrasea/Utilities/Less/CompilerTest.php | 31 ++++++++ .../Utilities/Less/RecessDriverTest.php | 31 ++++++++ www/skins/account/account.less | 4 +- www/skins/account/responsive.less | 2 - www/skins/login/less/login.less | 7 +- www/skins/login/less/responsive.less | 2 - 24 files changed, 452 insertions(+), 96 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Core/Provider/LessBuilderServiceProvider.php create mode 100644 lib/Alchemy/Phrasea/Core/Provider/LessCompilerServiceProvider.php delete mode 100644 lib/Alchemy/Phrasea/Utilities/Compiler/RecessLessCompiler.php create mode 100644 lib/Alchemy/Phrasea/Utilities/Less/Builder.php create mode 100644 lib/Alchemy/Phrasea/Utilities/Less/Compiler.php create mode 100644 lib/Alchemy/Phrasea/Utilities/Less/RecessDriver.php create mode 100644 tests/Alchemy/Tests/Phrasea/Core/Provider/LessBuilderServiceProviderTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Core/Provider/LessCompilerServiceProviderTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/account.less create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDir/TestPlugin/login.less create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/account.less create mode 100644 tests/Alchemy/Tests/Phrasea/Plugin/Fixtures/PluginDirInstalled/TestPlugin/login.less create mode 100644 tests/Alchemy/Tests/Phrasea/Utilities/Less/BuilderTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Utilities/Less/CompilerTest.php create mode 100644 tests/Alchemy/Tests/Phrasea/Utilities/Less/RecessDriverTest.php delete mode 100644 www/skins/account/responsive.less delete mode 100644 www/skins/login/less/responsive.less diff --git a/lib/Alchemy/Phrasea/Application.php b/lib/Alchemy/Phrasea/Application.php index 090e8b5057..efcd24cdcd 100644 --- a/lib/Alchemy/Phrasea/Application.php +++ b/lib/Alchemy/Phrasea/Application.php @@ -88,6 +88,8 @@ use Alchemy\Phrasea\Core\Provider\FtpServiceProvider; use Alchemy\Geonames\GeonamesServiceProvider; use Alchemy\Phrasea\Core\Provider\InstallerServiceProvider; use Alchemy\Phrasea\Core\Provider\JMSSerializerServiceProvider; +use Alchemy\Phrasea\Core\Provider\LessBuilderServiceProvider; +use Alchemy\Phrasea\Core\Provider\LessCompilerServiceProvider; use Alchemy\Phrasea\Core\Provider\LocaleServiceProvider; use Alchemy\Phrasea\Core\Provider\NotificationDelivererServiceProvider; use Alchemy\Phrasea\Core\Provider\ORMServiceProvider; @@ -255,6 +257,8 @@ class Application extends SilexApplication $this->register(new PluginServiceProvider()); $this->register(new PHPExiftoolServiceProvider()); $this->register(new ReCaptchaServiceProvider()); + $this->register(new LessCompilerServiceProvider()); + $this->register(new LessBuilderServiceProvider()); $this['recaptcha.public-key'] = $this->share(function (Application $app) { if ($app['phraseanet.registry']->get('GV_captchas')) { diff --git a/lib/Alchemy/Phrasea/Command/Developer/LessCompiler.php b/lib/Alchemy/Phrasea/Command/Developer/LessCompiler.php index d434d1cb49..4262a6fa7d 100644 --- a/lib/Alchemy/Phrasea/Command/Developer/LessCompiler.php +++ b/lib/Alchemy/Phrasea/Command/Developer/LessCompiler.php @@ -12,11 +12,9 @@ namespace Alchemy\Phrasea\Command\Developer; use Alchemy\Phrasea\Command\Command; -use Alchemy\Phrasea\Exception\RuntimeException; use Alchemy\Phrasea\Utilities\Compiler\RecessLessCompiler; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Process\ProcessBuilder; /** * This command builds less file @@ -40,29 +38,6 @@ class LessCompiler extends Command */ protected function doExecute(InputInterface $input, OutputInterface $output) { - $files = array( - $this->container['root.path'] . '/www/skins/login/less/login.less' => $this->container['root.path'] . '/www/skins/build/login.css', - $this->container['root.path'] . '/www/skins/account/account.less' => $this->container['root.path'] . '/www/skins/build/account.css', - $this->container['root.path'] . '/www/assets/bootstrap/less/bootstrap.less' => $this->container['root.path'] . '/www/skins/build/bootstrap/css/bootstrap.css', - $this->container['root.path'] . '/www/assets/bootstrap/less/responsive.less' => $this->container['root.path'] . '/www/skins/build/bootstrap/css/bootstrap-responsive.css', - ); - - $output->writeln('Building Assets...'); - - $failures = 0; - $errors = array(); - foreach ($files as $lessFile => $target) { - $this->container['filesystem']->mkdir(dirname($target)); - $output->writeln(sprintf('Building %s', basename($lessFile))); - - try { - $this->recessLessCompiler->compile($target, $lessFile); - } catch (\Exception $e) { - $failures++; - $errors[] = $e->getMessage(); - } - } - $copies = array( $this->container['root.path'] . '/www/assets/bootstrap/img/glyphicons-halflings-white.png' => $this->container['root.path'] . '/www/skins/build/bootstrap/img/glyphicons-halflings-white.png', $this->container['root.path'] . '/www/assets/bootstrap/img/glyphicons-halflings.png' => $this->container['root.path'] . '/www/skins/build/bootstrap/img/glyphicons-halflings.png', @@ -73,14 +48,23 @@ class LessCompiler extends Command $this->container['filesystem']->copy($source, $target); } - if (0 === $failures) { - $output->writeln('Build done !'); + $files = array( + $this->container['root.path'] . '/www/skins/login/less/login.less' => $this->container['root.path'] . '/www/skins/build/login.css', + $this->container['root.path'] . '/www/skins/account/account.less' => $this->container['root.path'] . '/www/skins/build/account.css', + $this->container['root.path'] . '/www/assets/bootstrap/less/bootstrap.less' => $this->container['root.path'] . '/www/skins/build/bootstrap/css/bootstrap.css', + $this->container['root.path'] . '/www/assets/bootstrap/less/responsive.less' => $this->container['root.path'] . '/www/skins/build/bootstrap/css/bootstrap-responsive.css', + ); - return 0; + $output->writeln('Building Assets...'); + + if (false === $this->container['phraseanet.less-builder']->build($files)) { + $output->writeln(sprintf('Errors occured during the build %s', implode(', ', $this->container['phraseanet.less-builder']->getErrors()))); + + return 1; } - $output->writeln(sprintf('%d errors occured during the build %s', $failures, implode(', ', $errors))); + $output->writeln('Build done !'); - return 1; + return 0; } } diff --git a/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php index e241c92d98..31d2437faf 100644 --- a/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php +++ b/lib/Alchemy/Phrasea/Command/Plugin/AbstractPluginCommand.php @@ -37,5 +37,14 @@ abstract class AbstractPluginCommand extends Command $output->write("Updating config files..."); $this->container['plugins.autoloader-generator']->write($manifests); $output->writeln(" OK"); + + $output->write('Building Assets...'); + if (false === $this->container['phraseanet.less-builder']->build(array( + $this->container['root.path'] . '/www/skins/login/less/login.less' => $this->container['root.path'] . '/www/skins/build/login.css', + $this->container['root.path'] . '/www/skins/account/account.less' => $this->container['root.path'] . '/www/skins/build/account.css', + ))) { + $output->writeln(sprintf('Error(s) occured during the build %s', implode(', ', $this->container['phraseanet.less-builder']->getErrors()))); + } + $output->writeln(" OK"); } } diff --git a/lib/Alchemy/Phrasea/Core/Provider/LessBuilderServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/LessBuilderServiceProvider.php new file mode 100644 index 0000000000..43d3dee191 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/LessBuilderServiceProvider.php @@ -0,0 +1,30 @@ +share(function($app) { + return new LessBuilder($app['phraseanet.less-compiler'], $app['filesystem']); + }); + } + + public function boot(Application $app) + { + } +} diff --git a/lib/Alchemy/Phrasea/Core/Provider/LessCompilerServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/LessCompilerServiceProvider.php new file mode 100644 index 0000000000..f19a829f5a --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/LessCompilerServiceProvider.php @@ -0,0 +1,30 @@ +share(function($app) { + return LessCompiler::create($app); + }); + } + + public function boot(Application $app) + { + } +} \ No newline at end of file diff --git a/lib/Alchemy/Phrasea/Plugin/Management/AutoloaderGenerator.php b/lib/Alchemy/Phrasea/Plugin/Management/AutoloaderGenerator.php index 71a65b6ab2..40ed2822c6 100644 --- a/lib/Alchemy/Phrasea/Plugin/Management/AutoloaderGenerator.php +++ b/lib/Alchemy/Phrasea/Plugin/Management/AutoloaderGenerator.php @@ -28,11 +28,53 @@ class AutoloaderGenerator ->doWrite('autoload.php', $this->createLoader($manifests)) ->doWrite('services.php', $this->createServices($manifests)) ->doWrite('commands.php', $this->createCommands($manifests)) - ->doWrite('twig-paths.php', $this->createTwigPathsMap($manifests)); + ->doWrite('twig-paths.php', $this->createTwigPathsMap($manifests)) + ->doWrite('login.less', $this->createLoginLess($manifests)) + ->doWrite('account.less', $this->createAccountLess($manifests)); return $this; } + private function createLoginLess($manifests) + { + $buffer = <<pluginDirectory . DIRECTORY_SEPARATOR . $manifest->getName() . DIRECTORY_SEPARATOR . 'login.less'; + if (is_file($filepath)) { + $relativePath = $manifest->getName() . DIRECTORY_SEPARATOR . 'login.less'; + $buffer .= <<pluginDirectory . DIRECTORY_SEPARATOR . $manifest->getName() . DIRECTORY_SEPARATOR . 'account.less'; + if (is_file($filepath)) { + $relativePath = $manifest->getName() . DIRECTORY_SEPARATOR . 'account.less'; + $buffer .= <<pluginDirectory . DIRECTORY_SEPARATOR . $file, $data)) { diff --git a/lib/Alchemy/Phrasea/Utilities/Compiler/RecessLessCompiler.php b/lib/Alchemy/Phrasea/Utilities/Compiler/RecessLessCompiler.php deleted file mode 100644 index 96ee79f953..0000000000 --- a/lib/Alchemy/Phrasea/Utilities/Compiler/RecessLessCompiler.php +++ /dev/null @@ -1,59 +0,0 @@ -filesystem = $filesystem ?: new Filesystem(); - } - - public function compile($target, $files) - { - $this->filesystem->mkdir(dirname($target)); - - if (!$files instanceof \Traversable) { - $files = new \ArrayObject(is_array($files) ? $files : array($files)); - } - - $files = new ArrayCollection((array) $files); - - if ($files->forAll(function($file) { - return is_file($file); - })) { - throw new RuntimeException(realpath($files) . ' does not exists.'); - } - - if (!is_writable(dirname($target))) { - throw new RuntimeException(realpath(dirname($target)) . ' is not writable.'); - } - - $builder = ProcessBuilder::create(array_merge(array( - 'recess', - '--compile' - ), $files->toArray())); - - $process = $builder->getProcess(); - $process->run(); - - if (!$process->isSuccessful()) { - throw new RuntimeException(sprintf('An errord occured during the build %s', $process->getErrorOutput())); - } - - $this->filesystem->dumpFile($target, $process->getOutput()); - } -} \ No newline at end of file diff --git a/lib/Alchemy/Phrasea/Utilities/Less/Builder.php b/lib/Alchemy/Phrasea/Utilities/Less/Builder.php new file mode 100644 index 0000000000..5789aefcf8 --- /dev/null +++ b/lib/Alchemy/Phrasea/Utilities/Less/Builder.php @@ -0,0 +1,71 @@ +compiler = $compiler; + $this->filesystem = $filesystem; + } + + /** + * Build LESS files + * + * @param array $files + */ + public function build($files) + { + $failures = 0; + $this->errors = array(); + + foreach ($files as $lessFile => $target) { + $this->filesystem->mkdir(dirname($target)); + + try { + $this->compiler->compile($target, $lessFile); + } catch (\Exception $e) { + $failures++; + $this->errors[] = $e->getMessage(); + } + } + + return $this->hasErrors(); + } + + public function hasErrors() + { + return count($this->errors) === 0; + } + + public function getErrors() + { + return $this->errors; + } +} diff --git a/lib/Alchemy/Phrasea/Utilities/Less/Compiler.php b/lib/Alchemy/Phrasea/Utilities/Less/Compiler.php new file mode 100644 index 0000000000..5047546f09 --- /dev/null +++ b/lib/Alchemy/Phrasea/Utilities/Less/Compiler.php @@ -0,0 +1,69 @@ +filesystem = $filesystem; + $this->recess = $recess; + } + + public static function create(Application $app) + { + $binaries = $app['phraseanet.configuration']['binaries']; + + return new self($app['filesystem'], RecessDriver::create($binaries)); + } + + public function compile($target, $files) + { + $this->filesystem->mkdir(dirname($target)); + + if (!$files instanceof \Traversable) { + $files = new \ArrayObject(is_array($files) ? $files : array($files)); + } + + $files = new ArrayCollection((array) $files); + + if ($files->forAll(function($file) { + return is_file($file); + })) { + throw new RuntimeException($files . ' does not exists.'); + } + + if (!is_writable(dirname($target))) { + throw new RuntimeException(realpath(dirname($target)) . ' is not writable.'); + } + + $commands = $files->toArray(); + array_unshift($commands, '--compile'); + + try { + $output = $this->recess->command($commands); + $this->filesystem->dumpFile($target, $output); + } catch (ExecutionFailureException $e) { + throw new RuntimeException('Could not execute recess command.', $e->getCode(), $e); + } + } +} diff --git a/lib/Alchemy/Phrasea/Utilities/Less/RecessDriver.php b/lib/Alchemy/Phrasea/Utilities/Less/RecessDriver.php new file mode 100644 index 0000000000..be447d7948 --- /dev/null +++ b/lib/Alchemy/Phrasea/Utilities/Less/RecessDriver.php @@ -0,0 +1,36 @@ +get('recess.binaries', array('recess')); + + return static::load($binaries, $logger, $conf); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Core/Provider/LessBuilderServiceProviderTest.php b/tests/Alchemy/Tests/Phrasea/Core/Provider/LessBuilderServiceProviderTest.php new file mode 100644 index 0000000000..d7922afa9a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Core/Provider/LessBuilderServiceProviderTest.php @@ -0,0 +1,16 @@ +cleanup($files); @@ -65,6 +67,9 @@ class AutoloaderGeneratorTest extends \PHPUnit_Framework_TestCase $mapping = require $pluginsDir . '/twig-paths.php'; $this->assertSame(array('plugin-test-plugin' => $pluginsDir . '/test-plugin/views', $pluginsDir . '/test-plugin/views', $pluginsDir . '/test-plugin/twig-views'), $mapping); + $this->assertRegExp('#@import#', file_get_contents($pluginsDir . '/login.less')); + $this->assertRegExp('#@import#', file_get_contents($pluginsDir . '/account.less')); + $this->cleanup($files); } diff --git a/tests/Alchemy/Tests/Phrasea/Utilities/Less/BuilderTest.php b/tests/Alchemy/Tests/Phrasea/Utilities/Less/BuilderTest.php new file mode 100644 index 0000000000..811a0e761e --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Utilities/Less/BuilderTest.php @@ -0,0 +1,34 @@ +getMockBuilder('Alchemy\Phrasea\Utilities\Less\Compiler') + ->disableOriginalConstructor() + ->getMock(); + $compiler->expects($this->once())->method('compile'); + + $filesystem = $this->getMock('Symfony\Component\Filesystem\Filesystem'); + $filesystem->expects($this->once())->method('mkdir'); + + $builder = new Builder($compiler, $filesystem); + + $build = $builder->build(array( __FILE__ => __DIR__ . '/output.css')); + + $this->assertTrue($build); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Utilities/Less/CompilerTest.php b/tests/Alchemy/Tests/Phrasea/Utilities/Less/CompilerTest.php new file mode 100644 index 0000000000..036acd8562 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Utilities/Less/CompilerTest.php @@ -0,0 +1,31 @@ +getMock('Alchemy\BinaryDriver\BinaryInterface'); + $recessDriver->expects($this->once())->method('command'); + + $filesystem = $this->getMock('Symfony\Component\Filesystem\Filesystem'); + $filesystem->expects($this->once())->method('mkdir'); + $filesystem->expects($this->once())->method('dumpFile'); + + $compiler = new Compiler($filesystem, $recessDriver); + + $compiler->compile(__DIR__ . '/output.css', __FILE__); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Utilities/Less/RecessDriverTest.php b/tests/Alchemy/Tests/Phrasea/Utilities/Less/RecessDriverTest.php new file mode 100644 index 0000000000..0543e1b314 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Utilities/Less/RecessDriverTest.php @@ -0,0 +1,31 @@ +assertInstanceOf('Alchemy\BinaryDriver\BinaryInterface', $recessDriver); + } + + public function testGetName() + { + $recessDriver = RecessDriver::create(); + + $this->assertEquals('recess', $recessDriver->getName()); + } +} diff --git a/www/skins/account/account.less b/www/skins/account/account.less index e8df9f5db4..b2d346b460 100644 --- a/www/skins/account/account.less +++ b/www/skins/account/account.less @@ -4,6 +4,7 @@ // Core variables and mixins @import "../../assets/bootstrap/less/variables.less"; @import "variables.less"; +@import "../../../plugins/account.less"; @import "../../assets/bootstrap/less/mixins.less"; // Grid system and page structure @@ -55,6 +56,7 @@ // Responsive @import "../../assets/bootstrap/less/responsive.less"; -@import "responsive.less"; +@import "variables.less"; +@import "../../../plugins/account.less"; @import "skin.less"; diff --git a/www/skins/account/responsive.less b/www/skins/account/responsive.less deleted file mode 100644 index c4f2ac873b..0000000000 --- a/www/skins/account/responsive.less +++ /dev/null @@ -1,2 +0,0 @@ - -@import "variables.less"; \ No newline at end of file diff --git a/www/skins/login/less/login.less b/www/skins/login/less/login.less index 204f3017a2..51a0246475 100644 --- a/www/skins/login/less/login.less +++ b/www/skins/login/less/login.less @@ -3,7 +3,10 @@ // Core variables and mixins @import "../../../assets/bootstrap/less/variables.less"; + @import "variables.less"; +@import "../../../../plugins/login.less"; + @import "../../../assets/bootstrap/less/mixins.less"; // Grid system and page structure @@ -55,6 +58,8 @@ // Responsive @import "../../../assets/bootstrap/less/responsive.less"; -@import "responsive.less"; + +@import "variables.less"; +@import "../../../../plugins/login.less"; @import "skin.less"; diff --git a/www/skins/login/less/responsive.less b/www/skins/login/less/responsive.less deleted file mode 100644 index c4f2ac873b..0000000000 --- a/www/skins/login/less/responsive.less +++ /dev/null @@ -1,2 +0,0 @@ - -@import "variables.less"; \ No newline at end of file