Implement bootstrap variable homepage customization through plugin system

This commit is contained in:
Nicolas Le Goff
2013-07-09 18:28:13 +02:00
parent cb3389ace9
commit c163f6c4ce
24 changed files with 452 additions and 96 deletions

View File

@@ -88,6 +88,8 @@ use Alchemy\Phrasea\Core\Provider\FtpServiceProvider;
use Alchemy\Geonames\GeonamesServiceProvider; use Alchemy\Geonames\GeonamesServiceProvider;
use Alchemy\Phrasea\Core\Provider\InstallerServiceProvider; use Alchemy\Phrasea\Core\Provider\InstallerServiceProvider;
use Alchemy\Phrasea\Core\Provider\JMSSerializerServiceProvider; 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\LocaleServiceProvider;
use Alchemy\Phrasea\Core\Provider\NotificationDelivererServiceProvider; use Alchemy\Phrasea\Core\Provider\NotificationDelivererServiceProvider;
use Alchemy\Phrasea\Core\Provider\ORMServiceProvider; use Alchemy\Phrasea\Core\Provider\ORMServiceProvider;
@@ -255,6 +257,8 @@ class Application extends SilexApplication
$this->register(new PluginServiceProvider()); $this->register(new PluginServiceProvider());
$this->register(new PHPExiftoolServiceProvider()); $this->register(new PHPExiftoolServiceProvider());
$this->register(new ReCaptchaServiceProvider()); $this->register(new ReCaptchaServiceProvider());
$this->register(new LessCompilerServiceProvider());
$this->register(new LessBuilderServiceProvider());
$this['recaptcha.public-key'] = $this->share(function (Application $app) { $this['recaptcha.public-key'] = $this->share(function (Application $app) {
if ($app['phraseanet.registry']->get('GV_captchas')) { if ($app['phraseanet.registry']->get('GV_captchas')) {

View File

@@ -12,11 +12,9 @@
namespace Alchemy\Phrasea\Command\Developer; namespace Alchemy\Phrasea\Command\Developer;
use Alchemy\Phrasea\Command\Command; use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Utilities\Compiler\RecessLessCompiler; use Alchemy\Phrasea\Utilities\Compiler\RecessLessCompiler;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\ProcessBuilder;
/** /**
* This command builds less file * This command builds less file
@@ -40,29 +38,6 @@ class LessCompiler extends Command
*/ */
protected function doExecute(InputInterface $input, OutputInterface $output) 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( $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-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', $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); $this->container['filesystem']->copy($source, $target);
} }
if (0 === $failures) { $files = array(
$output->writeln('<info>Build done !</info>'); $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('<error>Errors occured during the build %s</error>', implode(', ', $this->container['phraseanet.less-builder']->getErrors())));
return 1;
} }
$output->writeln(sprintf('<error>%d errors occured during the build %s</error>', $failures, implode(', ', $errors))); $output->writeln('<info>Build done !</info>');
return 1; return 0;
} }
} }

View File

@@ -37,5 +37,14 @@ abstract class AbstractPluginCommand extends Command
$output->write("Updating config files..."); $output->write("Updating config files...");
$this->container['plugins.autoloader-generator']->write($manifests); $this->container['plugins.autoloader-generator']->write($manifests);
$output->writeln(" <comment>OK</comment>"); $output->writeln(" <comment>OK</comment>");
$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>Error(s) occured during the build %s</error>', implode(', ', $this->container['phraseanet.less-builder']->getErrors())));
}
$output->writeln(" <comment>OK</comment>");
} }
} }

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Alchemy\Phrasea\Utilities\Less\Builder as LessBuilder;
class LessBuilderServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['phraseanet.less-builder'] = $app->share(function($app) {
return new LessBuilder($app['phraseanet.less-compiler'], $app['filesystem']);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Provider;
use Silex\Application;
use Silex\ServiceProviderInterface;
use Alchemy\Phrasea\Utilities\Less\Compiler as LessCompiler;
class LessCompilerServiceProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$app['phraseanet.less-compiler'] = $app->share(function($app) {
return LessCompiler::create($app);
});
}
public function boot(Application $app)
{
}
}

View File

@@ -28,11 +28,53 @@ class AutoloaderGenerator
->doWrite('autoload.php', $this->createLoader($manifests)) ->doWrite('autoload.php', $this->createLoader($manifests))
->doWrite('services.php', $this->createServices($manifests)) ->doWrite('services.php', $this->createServices($manifests))
->doWrite('commands.php', $this->createCommands($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; return $this;
} }
private function createLoginLess($manifests)
{
$buffer = <<<EOF
// This file is automatically generated, please do not edit it.
EOF;
foreach ($manifests as $manifest) {
$filepath = $this->pluginDirectory . DIRECTORY_SEPARATOR . $manifest->getName() . DIRECTORY_SEPARATOR . 'login.less';
if (is_file($filepath)) {
$relativePath = $manifest->getName() . DIRECTORY_SEPARATOR . 'login.less';
$buffer .= <<<EOF
@import "$relativePath";
EOF;
}
}
return $buffer;
}
private function createAccountLess($manifests)
{
$buffer = <<<EOF
// This file is automatically generated, please do not edit it.
EOF;
foreach ($manifests as $manifest) {
$filepath = $this->pluginDirectory . DIRECTORY_SEPARATOR . $manifest->getName() . DIRECTORY_SEPARATOR . 'account.less';
if (is_file($filepath)) {
$relativePath = $manifest->getName() . DIRECTORY_SEPARATOR . 'account.less';
$buffer .= <<<EOF
@import "$relativePath";
EOF;
}
}
return $buffer;
}
private function doWrite($file, $data) private function doWrite($file, $data)
{ {
if (false === file_put_contents($this->pluginDirectory . DIRECTORY_SEPARATOR . $file, $data)) { if (false === file_put_contents($this->pluginDirectory . DIRECTORY_SEPARATOR . $file, $data)) {

View File

@@ -1,59 +0,0 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Utilities\Compiler;
use Symfony\Component\Filesystem\Filesystem;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Process\ProcessBuilder;
class RecessLessCompiler
{
public function __construct($filesystem = null)
{
$this->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());
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Utilities\Less;
use Alchemy\Phrasea\Utilities\Less\Compiler as LessCompiler;
use Symfony\Component\Filesystem\Filesystem;
class Builder
{
/**
* @var LessCompiler
*/
protected $compiler;
/**
* @var Filesystem
*/
protected $filesystem;
/**
* @var array
*/
protected $errors = array();
public function __construct(LessCompiler $compiler, Filesystem $filesystem)
{
$this->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;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Utilities\Less;
use Alchemy\Phrasea\Application;
use Alchemy\BinaryDriver\BinaryInterface;
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
use Alchemy\Phrasea\Exception\RuntimeException;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Filesystem\Filesystem;
class Compiler
{
private $filesystem;
private $recess;
public function __construct(Filesystem $filesystem, BinaryInterface $recess)
{
$this->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);
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Utilities\Less;
use Alchemy\BinaryDriver\AbstractBinary;
use Psr\Log\LoggerInterface;
use Alchemy\BinaryDriver\Configuration;
use Alchemy\BinaryDriver\ConfigurationInterface;
class RecessDriver extends AbstractBinary
{
public function getName()
{
return 'recess';
}
public static function create($conf = array(), LoggerInterface $logger = null)
{
if (!$conf instanceof ConfigurationInterface) {
$conf = new Configuration($conf);
}
$binaries = $conf->get('recess.binaries', array('recess'));
return static::load($binaries, $logger, $conf);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
/**
* @covers Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider
*/
class LessCompilerServiceProvidertest extends ServiceProviderTestCase
{
public function provideServiceDescription()
{
return array(
array('Alchemy\Phrasea\Core\Provider\LessCompilerServiceProvider', 'phraseanet.less-compiler', '\Alchemy\Phrasea\Utilities\Less\Compiler'),
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Provider;
/**
* @covers Alchemy\Phrasea\Core\Provider\UnicodeServiceProvider
*/
class LessBuilderServiceProvidertest extends ServiceProviderTestCase
{
public function provideServiceDescription()
{
return array(
array('Alchemy\Phrasea\Core\Provider\LessBuilderServiceProvider', 'phraseanet.less-builder', '\Alchemy\Phrasea\Utilities\Less\Builder'),
);
}
}

View File

@@ -0,0 +1 @@
@import "my-plugin/account.less";

View File

@@ -0,0 +1 @@
@import "my-plugin/login.less";

View File

@@ -0,0 +1 @@
@import "my-plugin/account.less";

View File

@@ -0,0 +1 @@
@import "my-plugin/login.less";

View File

@@ -21,6 +21,8 @@ class AutoloaderGeneratorTest extends \PHPUnit_Framework_TestCase
$pluginsDir . '/autoload.php', $pluginsDir . '/autoload.php',
$pluginsDir . '/commands.php', $pluginsDir . '/commands.php',
$pluginsDir . '/twig-paths.php', $pluginsDir . '/twig-paths.php',
$pluginsDir . '/login.less',
$pluginsDir . '/account.less',
); );
$this->cleanup($files); $this->cleanup($files);
@@ -65,6 +67,9 @@ class AutoloaderGeneratorTest extends \PHPUnit_Framework_TestCase
$mapping = require $pluginsDir . '/twig-paths.php'; $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->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); $this->cleanup($files);
} }

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Tests\Phrasea\Utilities\Compiler;
use Alchemy\Phrasea\Utilities\Less\Builder;
class BuilderTest extends \PHPUnit_Framework_TestCase
{
public function testBuildSuccess()
{
$compiler = $this->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);
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Tests\Phrasea\Utilities\Compiler;
use Alchemy\Phrasea\Utilities\Less\Compiler;
class CompilerTest extends \PHPUnit_Framework_TestCase
{
public function testCompileSuccess()
{
$recessDriver = $this->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__);
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Tests\Phrasea\Utilities\Less;
use Alchemy\Phrasea\Utilities\Less\RecessDriver;
class RecessDriverTest extends \PHPUnit_Framework_TestCase
{
public function testGetCreate()
{
$recessDriver = RecessDriver::create();
$this->assertInstanceOf('Alchemy\BinaryDriver\BinaryInterface', $recessDriver);
}
public function testGetName()
{
$recessDriver = RecessDriver::create();
$this->assertEquals('recess', $recessDriver->getName());
}
}

View File

@@ -4,6 +4,7 @@
// Core variables and mixins // Core variables and mixins
@import "../../assets/bootstrap/less/variables.less"; @import "../../assets/bootstrap/less/variables.less";
@import "variables.less"; @import "variables.less";
@import "../../../plugins/account.less";
@import "../../assets/bootstrap/less/mixins.less"; @import "../../assets/bootstrap/less/mixins.less";
// Grid system and page structure // Grid system and page structure
@@ -55,6 +56,7 @@
// Responsive // Responsive
@import "../../assets/bootstrap/less/responsive.less"; @import "../../assets/bootstrap/less/responsive.less";
@import "responsive.less"; @import "variables.less";
@import "../../../plugins/account.less";
@import "skin.less"; @import "skin.less";

View File

@@ -1,2 +0,0 @@
@import "variables.less";

View File

@@ -3,7 +3,10 @@
// Core variables and mixins // Core variables and mixins
@import "../../../assets/bootstrap/less/variables.less"; @import "../../../assets/bootstrap/less/variables.less";
@import "variables.less"; @import "variables.less";
@import "../../../../plugins/login.less";
@import "../../../assets/bootstrap/less/mixins.less"; @import "../../../assets/bootstrap/less/mixins.less";
// Grid system and page structure // Grid system and page structure
@@ -55,6 +58,8 @@
// Responsive // Responsive
@import "../../../assets/bootstrap/less/responsive.less"; @import "../../../assets/bootstrap/less/responsive.less";
@import "responsive.less";
@import "variables.less";
@import "../../../../plugins/login.less";
@import "skin.less"; @import "skin.less";

View File

@@ -1,2 +0,0 @@
@import "variables.less";