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

Allow asset cache busting if not combining assets #1404

Closed
wants to merge 2 commits into from
Closed

Allow asset cache busting if not combining assets #1404

wants to merge 2 commits into from

Conversation

Toflar
Copy link
Member

@Toflar Toflar commented Feb 28, 2018

If you do not enable the checkbox to combine scripts in the layout, I think we should add a simple string to the file urls so we can benefit from cache busting. I'll add another PR for the manager-bundle to renable asset caching again for all assets by default.

@aschempp
Copy link
Member

aschempp commented Mar 1, 2018

I'm a little worried this is not entirely correct. First of all, the time might already be passed to the combiner, we should use that value then. Also, paths might be absolute/external, not sure we would need to handle that case here?

Honestly, I think we should rather move away from the combiner if we don't combine scripts...

@Toflar
Copy link
Member Author

Toflar commented Mar 1, 2018

First of all, the time might already be passed to the combiner, we should use that value then.

We do.

Honestly, I think we should rather move away from the combiner if we don't combine scripts...

That's not related to this PR. I don't want to touch existing logic. This is already the case, I just added a version flag, that's all.

@leofeyer
Copy link
Member

leofeyer commented Mar 1, 2018

If you do not enable the checkbox to combine scripts in the layout

Which is however enabled by default and likely to be removed in a future version, because we came to the conclusion that combining scripts also makes sense with HTTP/2.

How does the Symfony asset component handle this?

@Toflar
Copy link
Member Author

Toflar commented Mar 1, 2018

because we came to the conclusion that combining scripts also makes sense with HTTP/2.

I disagree with this. If you have a page where layout files change often, it does make sense to keep them separately. If you want to cover all use cases there's only one way: The admins have to be able to create bundles themselves and specify stuff like preload/prefetch etc. on their own for every bundle. However, I don't really think it's Contao's task to have support for pre-processing, bundling, optimizing, minifying etc. files because there are so many front end packaging tools like webpack, browserify, brunch, make, parcel, rollup and what not for this. They will always do that better than we ever will. We should focus on managing content, not front end tooling. That's definitely something we should all discuss about to find a general solution for the future but for now, this PR totally makes sense to me.

@aschempp
Copy link
Member

aschempp commented Mar 5, 2018

Which is however enabled by default and likely to be removed in a future version, because we came to the conclusion that combining scripts also makes sense with HTTP/2.

We agreed that there are situations where this still makes sense in HTTP/2. The problem is that is should be possible to create multiple bundles based on files that are always used on the same pages. But Contao does not offer that, so with HTTP/2 it could be better to not combine instead of create a different combination on each page essentially causing multiple downloads of the same stuff for no reason.

I do like cache busting very much, but I don't use Contao for that anymore. Because I don't trust it for some reason, maybe because it did not work for me for years. Using Symfony Encore creates files with random names so cache busting always works correctly. I've been using it in multiple projects by now, I think we should come up with a future solution using it and leave the existing one for legacy support and "non-optimized" setups.

@leofeyer
Copy link
Member

leofeyer commented Mar 5, 2018

Symfony Encore might be worth a shot. 👍

@Toflar
Copy link
Member Author

Toflar commented Mar 5, 2018

I agree. We might just generally thing about better support/integration of other tools that are better suited for all of this. For for now, we still have this checkbox and therefore that PR makes sense to me.

@aschempp
Copy link
Member

aschempp commented Mar 5, 2018

Symfony Encore might be worth a shot. 👍

I'm already using and loving it :)

@fritzmg
Copy link
Contributor

fritzmg commented Mar 7, 2018

One issue I have with this PR is that it will not provide cache busting for scripts that are added elsewhere via \Template::generateScriptTag(…);. For example if you use externalJs in the page layout or $GLOBALS['TL_JAVASCRIPT'] without the |static flag.

@Toflar
Copy link
Member Author

Toflar commented Mar 7, 2018

Can you add it?

@fritzmg
Copy link
Contributor

fritzmg commented Mar 7, 2018

Hm, well, where should it be done? It could be done within generateScriptTag automatically as well, but I am not sure if it should be done there, since it isn't really the purpose of that function.

Otherwise it would have to be done manually in various places (4 occurrences in Contao\Controller and at least one occurrence in Contao\PageRegular).

@Toflar
Copy link
Member Author

Toflar commented Mar 8, 2018

Otherwise it would have to be done manually in various places (4 occurrences in Contao\Controller and at least one occurrence in Contao\PageRegular).

Then probably there, right?

@fritzmg
Copy link
Contributor

fritzmg commented Mar 12, 2018

The backend should also use cache busting, imho. Every once in a while errors like this happens and then we have to tell customers to clear the cache - which they always react a little disgruntled, because they do not see why they would have to do it and see it as a flaw of the web application.

@leofeyer
Copy link
Member

As discussed in Mumble on March 15th, the PR is incomplete. It should also handle the $GLOBALS['TL_CSS'] and $GLOBALS['TL_JAVASCRIPT'] etc. files.

But 👍 for the feature in general.

@Toflar
Copy link
Member Author

Toflar commented Mar 19, 2018

Alright, will work on a solution then 👍

@Toflar
Copy link
Member Author

Toflar commented Mar 19, 2018

There you go.

The backend should also use cache busting, imho. Every once in a while errors like this happens and then we have to tell customers to clear the cache - which they always react a little disgruntled, because they do not see why they would have to do it and see it as a flaw of the web application.

BTW, since Contao 4.5 this is already fixed for all the assets being supplied by the Core itself. Only $GLOBALS['TL_CSS'] and $GLOBALS['TL_JAVASCRIPT'] were missing like for e.g. the Dropzone stuff that's added in the BE but 99% is already fixed. The latest commit should also cover these, now. Feedback appreciated 😄

@leofeyer
Copy link
Member

@discordier also suggested not to check the mtime in production at all (use the file if its there). Does this make sense to you, too?

@leofeyer leofeyer added this to the 4.6.0 milestone Mar 19, 2018
@Toflar
Copy link
Member Author

Toflar commented Mar 19, 2018

No. Can you elaborate?

@leofeyer
Copy link
Member

We are now checking the mtime to determine whether a file needs to be recompiled in development and production. However, similar to the Symfony cache, we should probably only check and recompile in development and not in production.

@fritzmg
Copy link
Contributor

fritzmg commented Mar 19, 2018

Why should it be only done in development? Doing so would make cache busting pointless.

@Toflar
Copy link
Member Author

Toflar commented Mar 19, 2018

Yeah we could add hundreds of optimizations but I won't do them in legacy code. If we find an alternative for these horrible global arrays I'll happily think about efficiency :)

@leofeyer
Copy link
Member

Doing so would make cache busting pointless.

The Symfony cache is not "busted" in production, either. If you change something, you have to clear it. This saves hundreds of filemtime() checks and therefore is not pointless at all.

@fritzmg
Copy link
Contributor

fritzmg commented Mar 19, 2018

Oh I see what you mean now.

@fritzmg
Copy link
Contributor

fritzmg commented Mar 20, 2018

BTW, since Contao 4.5 this is already fixed for all the assets being supplied by the Core itself.

Weird. But our customers are still affected by the aforementioned bug, if they do not clear their browser cache.

// edit: oh, it is not implemented in Contao 4.4.

@Toflar
Copy link
Member Author

Toflar commented Mar 20, 2018

// edit: oh, it is not implemented in Contao 4.4.

Yeah, it's not a bug. It's a feature :)

{
if (file_exists(TL_ROOT . '/' . $href))
Copy link
Member

@leofeyer leofeyer Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $mtime is not null, we do not need this file_exist() check, do we?

if ($mtime === null && file_exists(TL_ROOT . '/' . $href))
{
	$mtime = filemtime(TL_ROOT . '/' . $href);
}

@leofeyer
Copy link
Member

leofeyer commented Apr 5, 2018

It seems that the suffix is added twice if the asset URL comes from the Symfony asset component:

<link rel="stylesheet" href="bundles/contaocalendar/calendar.min.css?324ac593?d41d8cd9">

@leofeyer
Copy link
Member

leofeyer commented Apr 5, 2018

Also it does not handle external resources:

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans?d41d8cd9">

@leofeyer
Copy link
Member

leofeyer commented Apr 5, 2018

And last but not least we should not apply it to combined assets:

<script src="assets/js/jquery.min.js-f15f81ff.js?ec56bbf4"></script>

The combined file URL already contains f15f81ff in this case an I am pretty sure that this changes if one of the source files changes.

@Toflar
Copy link
Member Author

Toflar commented Apr 6, 2018

I cannot reproduce any of these cases. The problem is that there's no central routine and it happens at many places. Do you fixing the cases you discovered yourself directly?

@leofeyer
Copy link
Member

leofeyer commented Apr 6, 2018

This is how it works for me:

--- a/src/Resources/contao/classes/BackendTemplate.php
+++ b/src/Resources/contao/classes/BackendTemplate.php
@@ -78,7 +78,7 @@ class BackendTemplate extends \Template
 				}
 				else
 				{
-					$strStyleSheets .= \Template::generateStyleTag($this->addStaticUrlTo($stylesheet), $options->media);
+					$strStyleSheets .= \Template::generateStyleTag($this->addStaticUrlTo($stylesheet), $options->media, $options->mtime);
 				}
 			}
 
@@ -107,7 +107,7 @@ class BackendTemplate extends \Template
 				}
 				else
 				{
-					$strJavaScripts .= \Template::generateScriptTag($this->addStaticUrlTo($javascript), $options->async);
+					$strJavaScripts .= \Template::generateScriptTag($this->addStaticUrlTo($javascript), $options->async, $options->mtime);
 				}
 			}
 
--- a/src/Resources/contao/library/Contao/Combiner.php
+++ b/src/Resources/contao/library/Contao/Combiner.php
@@ -269,14 +269,23 @@ class Combiner extends \System
 	{
 		$return = $this->getFileUrls();
 
-		if ($this->strMode == self::JS)
+		foreach ($return as $k=>$v)
 		{
-			return implode('"></script><script src="', $return);
+			$return[$k] = str_replace('|', '" media="', $v);
+
+			if (file_exists(TL_ROOT . '/' . $v))
+			{
+				$return[$k] .= '?' . substr(md5(filemtime(TL_ROOT . '/' . $v)), 0, 8);
+			}
+			elseif (file_exists(TL_ROOT . '/' . $this->strWebDir . '/' . $v))
+			{
+				$return[$k] .= '?' . substr(md5(filemtime(TL_ROOT . '/' . $this->strWebDir . '/' . $v)), 0, 8);
+			}
 		}
 
-		foreach ($return as $k=>$v)
+		if ($this->strMode == self::JS)
 		{
-			$return[$k] = str_replace('|', '" media="', $v);
+			return implode('"></script><script src="', $return);
 		}
 
 		return implode('"><link rel="stylesheet" href="', $return);

--- a/src/Resources/contao/library/Contao/Template.php
+++ b/src/Resources/contao/library/Contao/Template.php
@@ -506,11 +506,36 @@ abstract class Template extends \Controller
 	 *
 	 * @param string $href  The script path
 	 * @param string $media The media type string
+	 * @param integer $mtime The file mtime
 	 *
 	 * @return string The markup string
 	 */
-	public static function generateStyleTag($href, $media=null)
+	public static function generateStyleTag($href, $media=null, $mtime=null)
 	{
+		// Add the filemtime if not given and not an external or combined file
+		if ($mtime === null && !preg_match('@(?:^https?://)|(?:-[a-f0-9]{8}\.css$)@', $href))
+		{
+			if (file_exists(TL_ROOT . '/' . $href))
+			{
+				$mtime = filemtime(TL_ROOT . '/' . $href);
+			}
+			else
+			{
+				$webDir = \StringUtil::stripRootDir(\System::getContainer()->getParameter('contao.web_dir'));
+
+				// Handle public bundle resources in web/
+				if (file_exists(TL_ROOT . '/' . $webDir . '/' . $href))
+				{
+					$mtime = filemtime(TL_ROOT . '/' . $webDir . '/' . $href);
+				}
+			}
+		}
+
+		if ($mtime !== null)
+		{
+			$href .= '?' . substr(md5($mtime), 0, 8);
+		}
+
 		return '<link rel="stylesheet" href="' . $href . '"' . (($media && $media != 'all') ? ' media="' . $media . '"' : '') . '>';
 	}
 
@@ -533,11 +558,36 @@ abstract class Template extends \Controller
 	 *
 	 * @param string  $src   The script path
 	 * @param boolean $async True to add the async attribute
+	 * @param integer $mtime The file mtime
 	 *
 	 * @return string The markup string
 	 */
-	public static function generateScriptTag($src, $async=false)
+	public static function generateScriptTag($src, $async=false, $mtime=null)
 	{
+		// Add the filemtime if not given and not an external or combined file
+		if ($mtime === null && !preg_match('@(?:^https?://)|(?:-[a-f0-9]{8}\.js)@', $src))
+		{
+			if (file_exists(TL_ROOT . '/' . $src))
+			{
+				$mtime = filemtime(TL_ROOT . '/' . $src);
+			}
+			else
+			{
+				$webDir = \StringUtil::stripRootDir(\System::getContainer()->getParameter('contao.web_dir'));
+
+				// Handle public bundle resources in web/
+				if (file_exists(TL_ROOT . '/' . $webDir . '/' . $src))
+				{
+					$mtime = filemtime(TL_ROOT . '/' . $webDir . '/' . $src);
+				}
+			}
+		}
+
+		if ($mtime !== null)
+		{
+			$src .= '?' . substr(md5($mtime), 0, 8);
+		}
+
 		return '<script src="' . $src . '"' . ($async ? ' async' : '') . '></script>';
 	}
  • It skips external and combined URLs.
  • It handles public bundle resources in the web directory.
  • It only adds the suffix in the Combiner in debug mode.

Note that checking for combined URLs is done via regex, which I think is ugly and error-prone. Maybe we can come up with a better solution?

@leofeyer
Copy link
Member

leofeyer commented Apr 6, 2018

Also I don't know if we really need to add suffixes in debug mode? If you already access your application via a separate entry point, you might as well open the browser's dev tools. 😄

@leofeyer leofeyer self-assigned this Apr 6, 2018
@leofeyer
Copy link
Member

leofeyer commented Apr 6, 2018

I was able to solve it without the regex. Implemented in 9a42dbe and 58a91bb.

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

Successfully merging this pull request may close these issues.

4 participants