liip/LiipImagineBundle — pull request #520 by kostiklv (2014-10-26 23:54:33+00:00)
Title: Cache Warmer - command to pre-generate filtered images cache

The concept is the following - user has to create a service which will implement `WarmerInterface`. Such service should return a list of paths and (optionally) provide a way to mark paths as cached and clear this mark when asked to do so. The service should be tagged as `liip_imagine.cache.warmer` and given a 'warmer' name to be used in filter_sets.

Note, that a single filter can use multiple warmers (e.g. category images and product images are all resized with the same filters), as well as a single warmer can be used for multiple filters (e.g. same original product images are used for thumbs and for preview images)

Then, the command will use this warmer service to get all paths (in chunks) and create cache for them (for all the filters configured).

This is very helpful to avoid huge server load when rolling out new filters or just when starting to use the bundle on existing website. Without this, on sites with large traffic and a lot of images, client requests to not yet cached images could bring the server to its knees.

The proper way to use the command is to deploy new filter_set configuration with a warmer, run the command and when the cache is warmed - deploy the code which will actually use the new filters.

I haven't updated docs and fixed/added tests yet. If the concept is accepted - I'll do this

Also, there is one thing which bugs me - circular service dependency between `CacheWarmer` and `CacheManager`. I didn't find a way to get around this, so for now - I just used setter injection. If you have better ideas how to avoid this dependency - I'm all ears.

Here is configuration example:

``` yaml
services:
acme.imagine.warmer.products:
class: Acme\SomeBundle\Imagine\ProductImagesWarmer
arguments:
- "@doctrine.orm.entity_manager"
tags:
- { name: 'liip_imagine.cache.warmer', warmer: 'products_warmer' }

liip_imagine:
...
filter_sets:
product_thumb:
quality: 85
filters:
thumbnail: { size: [150, 150], mode: inset }
warmers: [products_warmer]
product_preview:
quality: 85
filters:
thumbnail: { size: [250, 250], mode: inset }
warmers: [products_warmer]
```

And a warmer class:
``` php
em = $em;
}

/** @inheritdoc */
public function getPaths($start, $chunk)
{
$imagesRepo = $this->em->getRepository('AcmeSomeBundle:ProductImage');
$qb = $imagesRepo->createQueryBuilder('i');
$query = $qb->select('i')
->where('i.cacheWarmed = 0')
// we don't need to use setFirstResult($start) here, as we always query first $chunk of not warmed images
->setMaxResults($chunk)
->orderBy('i.id', 'asc')
->getQuery();

$images = $query->getResult();

$result = array();
foreach ($images as $row) {
$result[] = array(
'path' => $row->getImagePath(),
'object' => $row
);
}

return $result;
}

/** @inheritdoc */
public function setWarmed($data)
{
foreach ($data as $path) {
if (isset($path['object']) && $path['object'] instanceof ProductImage) {
$path['object']->setCacheWarmed(1);
}
}
$this->em->flush();
$this->em->clear();
gc_collect_cycles();
}

/** @inheritdoc */
public function clearWarmed($paths = null)
{
if (!isset($paths) || empty($paths)) {
// reset warmed state for all images
$qb = $this->em->createQueryBuilder();
$qb->update('AcmeSomeBundle:ProductImage', 'i')
->set('i.cacheWarmed', 0)
->getQuery()
->execute();
return;
}
if (!is_array($paths)) {
$paths = array($paths);
}

foreach ($paths as $path) {
if ($image = $this->getImageByPath($path)) {
$image->setCacheWarmed(0);
}
}
$this->em->flush();
}

protected function getImageByPath($path)
{
$imagesRepo = $this->em->getRepository('AcmeSomeBundle:ProductImage');
$image = $imagesRepo->findOneByImagePath($path);
return $image;
}
}
```