Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glide 2.0 or future development #288

Open
rreynier opened this issue Oct 9, 2020 · 7 comments
Open

Glide 2.0 or future development #288

rreynier opened this issue Oct 9, 2020 · 7 comments

Comments

@rreynier
Copy link

rreynier commented Oct 9, 2020

Are there plans to continue development on Glide 2.0? It seems like the project is essentially in maintenance mode (which is fine). Just curious what the long term plans are for the project moving forward.

@reinink
Copy link
Contributor

reinink commented Oct 22, 2020

Great question @rreynier!

I am excited to announce that, as of today, @tgalopin has officially taken over as the core maintainer of this project. 🙌

As I announced on Twitter, as much as I love it, I simply don't have the capacity for this project any longer. Passing this project off to Titouan felt like the right thing to do for Glide. 👍

Titouan actually reached out to me months ago, showing interest in this project, and sharing some vision he has for it, which is really cool!

That all said, I'll probably be around a little still during the transition phase...helping Titouan as needed. I think Titouan is planning to get into the project in the next month or two.

@ADmad ADmad mentioned this issue Oct 24, 2020
@tgalopin
Copy link
Member

I profit from this issue and the fact that I (finally) have some time to dedicate to Glide to explain what my ideas are.

Warning: long answer :D

Note that these ideas are what I propose but I'm fully open to suggestions too, I see open-source as community-driven: if you have a need and open a PR, my default opinion will be to accept it unless there is a good reason not to.

In the short term, there are two things I'm working on:

  • merging open pull requests that do not break Backward Compatibility in Glide 1 to ease usage of people having put the work
  • close pull requests which are not relevant anymore for whatever reason
  • then merging Flysystem 2.0 support, thus releasing Glide 2.0 : this new major is necessary because Flysystem 2.0 is incompatible with 1.0, but the 2.0 release will only contain Flysystem 2.0 support. At this point, new features will only be added to Glide 2.0 and Glide 1.0 will be supported for bugs and security issues.
  • finally, I have a few key interesting elements in mind for Glide 3.0, which could come quite soon after Glide 2.0, described a bit better below

This roadmap will 1/ help people using 1.0 to keep using it (with necessary features) for a while, but at the same time 2/ provide an easy migration path to Flysystem 2.0 as Glide 2.0 will only focus on this upgrade, thus limiting the struggle to upgrade, and finally 3/ in 3.0 we will be able to go further in terms of BC breaks.

Glide 3.0

For Glide 3.0, I proposed several ideas to Jonathan a while ago that still applies today IMO. I would like to rethink the library developer experience to ease its usage, simplify its code and improve its capabilities.

Here is how it could look like in terms of usage:

$glide = Glide::create([
    // Flysystem, source for the images data
    // Required
    'source' => $source,
   
    // For the cache, I would like to use Symfony Cache instead, for reasons detailed below
    // Required
    'cache' => $cache,

    // The response factory to use (default to HttpFoundation, as it's the most common one)
    'response_factory' => $responseFactory,

    // Equivalent of the "API" in Glide 1.0, but I think this naming is clearer
    // This object will dispatch the manipulation to an array of manipulators
    // If not provided, creates a default one
    'image_manipulator' => $imageManipulator,

    // Secret used to sign image requests, if null signing is disabled
    'secret' => 'af3dc18fc6bfb2afb521e587c348b904',

    // Maximum image size that Glide will accept to render, null to have no limit
    'max_image_size' => 2000*2000,

    // Default parameters for image storage handling
    'default_storage_parameters' => [
        'w' => 2000,
        'h' => 2000,
        'fit' => 'contain',
        'fm' => 'pjpg',
        'q' => 80,
    ],

    // Presets usable in parameters (?p=user-picture-small)
    'presets' => [
        'user-picture-small' => [
            'w' => 200,
            'h' => 200,
            'fit' => 'crop',
        ],
    ],
]);

// Apply initial manipulations to an image and store it in the source at a given path
// Here, the image is resized and re-encoded at storage to decrease the stored file size
// Easier to understand/manipulate than the source Filesystem directly (still possible of course)
$glide->store('users/'.$username.'.jpeg', file_get_contents($uploadedFile), [
    'w' => 2000,
    'h' => 2000,
    'fit' => 'contain',
    'fm' => 'pjpg',
    'q' => 80,
]);

// Check whether the given path exists in the source filesystem
$glide->has('users/'.$username.'.jpeg');

// Remove an image from the source filesystem
$glide->remove('users/'.$username.'.jpeg');

// Clear the cache for a given path
$glide->clearCache('users/'.$username.'.jpeg');

// Clear the cache for all paths
$glide->clearAllCache();

// Sign a given request path and parameters and return the signature
// Returns an empty string if no secret was provided as configuration
$glide->sign('users/'.$username.'.jpeg', [
    'w' => 200,
    'h' => 200,
    'fit' => 'crop',
]);

// Handles a given request with parameters and return a Response using the ResponseFactory
// Throws a NotFoundException if the image doesn't exist or the signature is not valid
// Throws a BadRequestException if the image size is too large or the parameters are not supported
// Exceptions are thrown by the ResponseFactory to profit from framework exceptions
$glide->handle('users/'.$username.'.jpeg', [
    'w' => 200,
    'h' => 200,
    'fit' => 'crop',
    's' => '9978a40f1fc75fa64ac92ea9baf16ff3',
]);

// Directly display an image by sending appropriate headers and content through native PHP calls
// Throws a NotFoundException if the image doesn't exist or the signature is not valid
// Throws a BadRequestException if the image size is too large of the parameters are not supported
// Exceptions are thrown by the ResponseFactory to profit from framework exceptions
$glide->display('users/'.$username.'.jpeg', [
    'w' => 200,
    'h' => 200,
    'fit' => 'crop',
    's' => '9978a40f1fc75fa64ac92ea9baf16ff3',
]);

// Create the base 64 representation of an image
// Throws a NotFoundException if the image doesn't exist or the signature is not valid
// Throws a BadRequestException if the image size is too large of the parameters are not supported
// Exceptions are thrown by the ResponseFactory to profit from framework exceptions
$glide->createBase64('users/'.$username.'.jpeg', [
    'w' => 200,
    'h' => 200,
    'fit' => 'crop',
    's' => '9978a40f1fc75fa64ac92ea9baf16ff3',
]);

These changes would make Glide even more straightforward IMO. Besides the API changes, there are 2 main updates:

  • the ability to store images directly using Glide, profiting from the ability to resize on storage, which decrease file size and improve runtime performance
  • the usage of Symfony Cache for cache instead of Flysytem

Using Symfony Cache has many advantages, the main one being the way it's designed internally. The CacheInterface defines only two methods: get and remove. remove is easy to understand but get is a bit different than the usual cache interfaces.

get receives 3 arguments:

  • the key to fetch from the cache
  • a callback to generate the value if it was not present in the cache
  • an extra parameter called $beta which I'll explain a bit later

With this structure, a typical cache fetching would look like the following:

$value = $cache->get('my-key', function(ItemInterface $item) {
    $item->expiresAfter(3600);

    // compute the value ...

    return $computedValue;
});

There are two execution paths possible here:

  • either the cache is empty for the provided key, in which case the callback is called to compute the value, it is stored in the cache and it is returned ;
  • or the cache is warm for this key, and the cached value is returned directly ;

This interface, in addition to being elegant in terms of DX, provides by default a huge advantage over other cache interfaces: it's extremely easy to add locking features around cache generation, avoiding multiple frontends to compute the same value at the same time. That's what the Symfony Cache component does by default: it uses the caching storage to store a lock of currently computed values and avoid multiple frontends taking the CPU/memory hit of computing the cache.

In our context, this is a huge advantage: computing multiple images is CPU-intensive and having multiple PHP-FPM workers computing the same cache on the same time is wasting resources.

The extra parameter, $beta, is referencing an additional feature of Symfony Cache: probabilistic expiration.

Locking is a great way to avoid multiple frontends to compute the same values but these frontends will still be blocked, waiting for the cache to be ready. Having multiple frontends blocked by a cache generation can be a big problem as it uses a network connection and a PHP-FPM worker.

Probabilistic expiration solves this by calculating an increasing probability to expire before the actual expiration date. This means that the cache will randomly be computed earlier than the configured expiration date, this time by a single frontend, and with a probability of doing so that increases as we approach the configured expiration date. This avoids to get all frontends to compute the cache at the same time.

By using Symfony Cache, Glide would profit from these powerful features without having to do anything, thus my proposal.

Big answer, but I thought it was interesting to expose my ideas :) . Feel free to send feedbacks!

@ADmad
Copy link
Collaborator

ADmad commented Jan 24, 2021

Linking the previous Glide 2.0 brainstorming issue #170 here for better visibility. "Serve cached images via web server" is one feature I would like to be realized.

@tgalopin
Copy link
Member

Absolutely, very good idea IMO. It's going to be interesting to see how we can do that.

This was referenced Jan 24, 2021
@barryvdh
Copy link
Member

I think you can already do something like this:

Path like: /media/something/jumbotron.jpg/glide/w/600/h/300/jumbotron3.jpg

<?php

$sourceFolder = '/var/www/public/media';
$cacheFolder = __DIR__ .'/../storage/cache';

require __DIR__ . '/../vendor/autoload.php';

// Split on the /glide/
list($path, $params) = explode('/glide/', $_SERVER["REQUEST_URI"]);

$params = explode('/', $params);
$filename = array_pop($params);

// Move the url into key/value pairs
$result = [];
while (count($params) >= 2) {
    list($key, $value) = array_splice($params, 0, 2);
    $result[$key] = $value;
}

// Setup Glide server
$server = League\Glide\ServerFactory::create([
    'source' => $sourceFolder,
    'cache' => $cacheFolder,
]);

// Store for direct access later
$source = $cacheFolder .'/'. $server->makeImage($path, $result);
$target = __DIR__ .'/../public/' . $_SERVER["REQUEST_URI"];
mkdir(dirname($target), 0777, true);
copy($source, $target);

// Serve for now
$server->outputImage($path, $result);

(Use the directories as prefix)
Downside is that your cache is doubled. So preferably you would use the cache directly. And you need to make sure you use the same ordering otherwise you end up with the same images over and over again.. (And perhaps some sanitizing like check the query params etc)

Downside is that you can't do auto content-negotiation (WebP)

@vitalijalbu

This comment was marked as off-topic.

@celsojr-websters

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants