Improved PreviewGenerator by composing an ImageBuilder that creates new Image objects fore each URL

This commit is contained in:
Alejandro Celaya 2016-08-18 12:21:26 +02:00
parent 15247e832e
commit 2c91ded514
12 changed files with 181 additions and 20 deletions

View file

@ -2,7 +2,6 @@
return [ return [
'phpwkhtmltopdf' => [ 'phpwkhtmltopdf' => [
'files_location' => 'data/cache',
'images' => [ 'images' => [
'binary' => 'bin/wkhtmltoimage', 'binary' => 'bin/wkhtmltoimage',
'type' => 'jpg', 'type' => 'jpg',

View file

@ -0,0 +1,8 @@
<?php
return [
'preview_generation' => [
'files_location' => 'data/cache',
],
];

View file

@ -10,6 +10,7 @@ return [
Command\Shortcode\ResolveUrlCommand::class, Command\Shortcode\ResolveUrlCommand::class,
Command\Shortcode\ListShortcodesCommand::class, Command\Shortcode\ListShortcodesCommand::class,
Command\Shortcode\GetVisitsCommand::class, Command\Shortcode\GetVisitsCommand::class,
Command\Shortcode\GeneratePreviewCommand::class,
Command\Visit\ProcessVisitsCommand::class, Command\Visit\ProcessVisitsCommand::class,
Command\Config\GenerateCharsetCommand::class, Command\Config\GenerateCharsetCommand::class,
Command\Config\GenerateSecretCommand::class, Command\Config\GenerateSecretCommand::class,

View file

@ -14,6 +14,7 @@ return [
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class, Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class, Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class, Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class, Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class, Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class, Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,

View file

@ -0,0 +1,87 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class GeneratePreviewCommand extends Command
{
/**
* @var PreviewGeneratorInterface
*/
private $previewGenerator;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* GeneratePreviewCommand constructor.
* @param ShortUrlServiceInterface $shortUrlService
* @param PreviewGeneratorInterface $previewGenerator
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class, PreviewGenerator::class, "translator"})
*/
public function __construct(
ShortUrlServiceInterface $shortUrlService,
PreviewGeneratorInterface $previewGenerator,
TranslatorInterface $translator
) {
$this->previewGenerator = $previewGenerator;
$this->translator = $translator;
$this->shortUrlService = $shortUrlService;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:process-previews')
->setDescription(
$this->translator->translate(
'Processes and generates the previews for every URL, improving performance for later web requests.'
)
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$page = 1;
do {
$shortUrls = $this->shortUrlService->listShortUrls($page);
$page += 1;
foreach ($shortUrls as $shortUrl) {
$this->processUrl($shortUrl->getOriginalUrl(), $output);
}
} while ($page <= $shortUrls->count());
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
}
protected function processUrl($url, OutputInterface $output)
{
try {
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
$this->previewGenerator->generatePreview($url);
$output->writeln($this->translator->translate(' <info>Success!</info>'));
} catch (PreviewGenerationException $e) {
$output->writeln([
' <error>' . $this->translator->translate('Error') . '</error>',
'<error>' . $e->__toString() . '</error>',
]);
}
}
}

View file

@ -2,14 +2,13 @@
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache; use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use mikehaertl\wkhtmlto\Image;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Shlinkio\Shlink\Common\Factory\LoggerFactory; use Shlinkio\Shlink\Common\Factory\LoggerFactory;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory; use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Shlinkio\Shlink\Common\Image\ImageFactory; use Shlinkio\Shlink\Common\Image;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Common\Service; use Shlinkio\Shlink\Common\Service;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension; use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
@ -24,12 +23,13 @@ return [
GuzzleHttp\Client::class => InvokableFactory::class, GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => CacheFactory::class, Cache::class => CacheFactory::class,
'Logger_Shlink' => LoggerFactory::class, 'Logger_Shlink' => LoggerFactory::class,
Image::class => ImageFactory::class,
Translator::class => TranslatorFactory::class, Translator::class => TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class, TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class, LocaleMiddleware::class => AnnotatedFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
Service\IpLocationResolver::class => AnnotatedFactory::class, Service\IpLocationResolver::class => AnnotatedFactory::class,
Service\PreviewGenerator::class => AnnotatedFactory::class, Service\PreviewGenerator::class => AnnotatedFactory::class,
], ],

View file

@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\AbstractPluginManager;
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{
protected $instanceOf = Image::class;
}

View file

@ -0,0 +1,31 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ImageBuilderFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ImageBuilder($container, ['factories' => [
Image::class => ImageFactory::class,
]]);
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use Zend\ServiceManager\ServiceLocatorInterface;
interface ImageBuilderInterface extends ServiceLocatorInterface
{
}

View file

@ -25,6 +25,12 @@ class ImageFactory implements FactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{ {
$config = $container->get('config')['phpwkhtmltopdf']; $config = $container->get('config')['phpwkhtmltopdf'];
return new Image(isset($config['images']) ? $config['images'] : null); $image = new Image(isset($config['images']) ? $config['images'] : null);
if (isset($options['url'])) {
$image->setPage($options['url']);
}
return $image;
} }
} }

View file

@ -5,13 +5,11 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Doctrine\Common\Cache\Cache; use Doctrine\Common\Cache\Cache;
use mikehaertl\wkhtmlto\Image; use mikehaertl\wkhtmlto\Image;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException; use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Image\ImageBuilder;
use Shlinkio\Shlink\Common\Image\ImageBuilderInterface;
class PreviewGenerator implements PreviewGeneratorInterface class PreviewGenerator implements PreviewGeneratorInterface
{ {
/**
* @var Image
*/
private $image;
/** /**
* @var Cache * @var Cache
*/ */
@ -20,20 +18,24 @@ class PreviewGenerator implements PreviewGeneratorInterface
* @var string * @var string
*/ */
private $location; private $location;
/**
* @var ImageBuilderInterface
*/
private $imageBuilder;
/** /**
* PreviewGenerator constructor. * PreviewGenerator constructor.
* @param Image $image * @param ImageBuilderInterface $imageBuilder
* @param Cache $cache * @param Cache $cache
* @param string $location * @param string $location
* *
* @Inject({Image::class, Cache::class, "config.phpwkhtmltopdf.files_location"}) * @Inject({ImageBuilder::class, Cache::class, "config.preview_generation.files_location"})
*/ */
public function __construct(Image $image, Cache $cache, $location) public function __construct(ImageBuilderInterface $imageBuilder, Cache $cache, $location)
{ {
$this->image = $image;
$this->cache = $cache; $this->cache = $cache;
$this->location = $location; $this->location = $location;
$this->imageBuilder = $imageBuilder;
} }
/** /**
@ -45,17 +47,19 @@ class PreviewGenerator implements PreviewGeneratorInterface
*/ */
public function generatePreview($url) public function generatePreview($url)
{ {
$cacheId = sprintf('preview_%s.%s', urlencode($url), $this->image->type); /** @var Image $image */
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
$cacheId = sprintf('preview_%s.%s', urlencode($url), $image->type);
if ($this->cache->contains($cacheId)) { if ($this->cache->contains($cacheId)) {
return $this->cache->fetch($cacheId); return $this->cache->fetch($cacheId);
} }
$path = $this->location . '/' . $cacheId; $path = $this->location . '/' . $cacheId;
$this->image->setPage($url); $image->saveAs($path);
$this->image->saveAs($path);
// Check if an error occurred // Check if an error occurred
$error = $this->image->getError(); $error = $image->getError();
if (! empty($error)) { if (! empty($error)) {
throw PreviewGenerationException::fromImageError($error); throw PreviewGenerationException::fromImageError($error);
} }

View file

@ -6,7 +6,9 @@ use mikehaertl\wkhtmlto\Image;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Image\ImageBuilder;
use Shlinkio\Shlink\Common\Service\PreviewGenerator; use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Zend\ServiceManager\ServiceManager;
class PreviewGeneratorTest extends TestCase class PreviewGeneratorTest extends TestCase
{ {
@ -27,7 +29,13 @@ class PreviewGeneratorTest extends TestCase
{ {
$this->image = $this->prophesize(Image::class); $this->image = $this->prophesize(Image::class);
$this->cache = new ArrayCache(); $this->cache = new ArrayCache();
$this->generator = new PreviewGenerator($this->image->reveal(), $this->cache, 'dir'); $this->generator = new PreviewGenerator(new ImageBuilder(new ServiceManager(), [
'factories' => [
Image::class => function () {
return $this->image->reveal();
},
]
]), $this->cache, 'dir');
} }
/** /**
@ -50,7 +58,6 @@ class PreviewGeneratorTest extends TestCase
$cacheId = sprintf('preview_%s.png', urlencode($url)); $cacheId = sprintf('preview_%s.png', urlencode($url));
$expectedPath = 'dir/' . $cacheId; $expectedPath = 'dir/' . $cacheId;
$this->image->setPage($url)->shouldBeCalledTimes(1);
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1); $this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
$this->image->getError()->willReturn('')->shouldBeCalledTimes(1); $this->image->getError()->willReturn('')->shouldBeCalledTimes(1);
@ -69,7 +76,6 @@ class PreviewGeneratorTest extends TestCase
$cacheId = sprintf('preview_%s.png', urlencode($url)); $cacheId = sprintf('preview_%s.png', urlencode($url));
$expectedPath = 'dir/' . $cacheId; $expectedPath = 'dir/' . $cacheId;
$this->image->setPage($url)->shouldBeCalledTimes(1);
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1); $this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
$this->image->getError()->willReturn('Error!!')->shouldBeCalledTimes(1); $this->image->getError()->willReturn('Error!!')->shouldBeCalledTimes(1);