mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 13:23:33 +03:00
Improved PreviewGenerator by composing an ImageBuilder that creates new Image objects fore each URL
This commit is contained in:
parent
15247e832e
commit
2c91ded514
12 changed files with 181 additions and 20 deletions
|
@ -2,7 +2,6 @@
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'phpwkhtmltopdf' => [
|
'phpwkhtmltopdf' => [
|
||||||
'files_location' => 'data/cache',
|
|
||||||
'images' => [
|
'images' => [
|
||||||
'binary' => 'bin/wkhtmltoimage',
|
'binary' => 'bin/wkhtmltoimage',
|
||||||
'type' => 'jpg',
|
'type' => 'jpg',
|
||||||
|
|
8
config/autoload/preview-generation.global.php
Normal file
8
config/autoload/preview-generation.global.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'preview_generation' => [
|
||||||
|
'files_location' => 'data/cache',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
87
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal file
87
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal 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>',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
],
|
],
|
||||||
|
|
10
module/Common/src/Image/ImageBuilder.php
Normal file
10
module/Common/src/Image/ImageBuilder.php
Normal 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;
|
||||||
|
}
|
31
module/Common/src/Image/ImageBuilderFactory.php
Normal file
31
module/Common/src/Image/ImageBuilderFactory.php
Normal 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,
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
}
|
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||||
|
|
||||||
|
interface ImageBuilderInterface extends ServiceLocatorInterface
|
||||||
|
{
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue