Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Refactored resource loading logic out of the resources #576

Open
wants to merge 1 commit into from

3 participants

@webmozart

I think that the current resource classes are very hard to understand (I spent a lot of time figuring out what they do). IMO that's because they have too many responsibilities:

  • they represent resources
  • they load resources (in case of DirectoryResource and CoalescingDirectoryResource)

For this reason, I refactored the resource loading logic out of the resource classes. DirectoryResource and CoalescingDirectoryResource are deprecated now. Their getFresh() and getContent() methods don't seem to be used at all.

If we need resource collections with collective getFresh() and getContent() methods, I suggest adding a ResourceCollection class instead and use that instead of arrays. So far, that didn't seem necessary.

Before:

$directory = new DirectoryResource('/views', '.twig');
$file = new FileResource('/bundle/views/layout.html.twig');

$am->setLoader('am', $loader);
$am->addResource($directory, 'twig');
$am->addResource($file, 'twig');

After:

$loader = new DirectoryLoader();
$file = new FileResource('/bundle/views/layout.html.twig');

$am->setLoader('am', $loader);

foreach ($loader->load('/views', '.twig') as $entry) {
    $am->addResource($entry, 'twig');
}
$am->addResource($file, 'twig');

For me, this is much easier to grasp.

Once the deprecation phase is over and DirectoryResource removed, the code for iterating directories can be removed from CachedFormulaLoader. Again, this makes sense in my opinion. All other formula loaders take resources which correspond to files, so passing directory resources here is confusing for me.

Before:

$directory = new DirectoryResource('/views', '.twig');
$formulaLoader = new TwigFormulaLoader($twig);
$cachedLoader = new CachedFormulaLoader($loader, $cache);

// doesn't work
$formulaLoader->load($directory);

// works (huh?)
$cachedLoader->load($directory);

After:

$directoryLoader = new DirectoryLoader();
$formulaLoader = new TwigFormulaLoader($twig);
$cachedLoader = new CachedFormulaLoader($loader, $cache);

$files = $directoryLoader->load('/views', '.twig');

foreach ($files as $file) {
    $formulaLoader->load($file);
    // or
    $cachedLoader->load($file);
}

Since I don't know what the backwards compatibility policy is for Assetic, I left all existing code in as deprecated and did not introduce any BC breaks.

@webmozart

Btw another simplification would be to replace ResourceInterface by FileInterface. As far as I see, all elementary resources (Twig templates, config files etc.) are files anyway (and the developer who reads the code (me) doesn't spend unnecessary time figuring out why that abstraction is needed, even though it isn't :) ). What do you think?

@stof
Collaborator

The issue I see here is that it will become impossible to register the routing resources in the AsseticBundle after this change, because the Symfony resource system suffers from the same issue (which is logical as it was the inspiration used when implementing them in Assetic). See symfony/symfony#7230

@stof
Collaborator

and the DirectoryResource is necessary to detect the case where you add a new file in a directory, so that it is parsed as well

@mpdude

Hi guys,

merely by accident I just found this issue. I have opened a few PRs/issues regarding the Resource-related concepts in Symfony a while ago which have caused me quite some headache:

There is also a PR for BC-compatible minimal changes in Symfony (symfony/symfony#7781) that would allow to introduce service-based Resource validation and also support non-filesystem-based or other custom Resources. That is currently designed to happen completely in userland. That might also help in fixing symfony/AsseticBundle#168.

My point is that we might find a way to design "assetic resources" however they work best for Assetic and then have bridging code in AsseticBundle that comes into play when need to check those "assetic resources" for freshness (-> rebuilding routes for the AsseticController).

Does that sound as if I could help?

@mpdude

bump

@stof
Collaborator

@mpdude The issue is that designing Assetic resources so that they are checked for freshness externally will not allow us to bridge them to Symfony resources after that (which is our real use case for Assetic resources in the first place), given that Symfony resources need to be self-contained.

So the Symfony architecture change is a prerequisite for us being able to solve thing here, unless we kill the integration in Symfony resources entirely (which is a pretty bad idea)

@mpdude

Yes, that Symfony change is what I'm after.

I think symfony/symfony#7781 contains the minimal changes I'd need to come up with a solution that can live outside of the core. The other two issues mentioned above also target at that but might be too broad to be easily discussed.

Anyway, I'd like if you two guys could have a look at symfony/symfony#7781. If you don't see how this might help here, let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
4 CHANGELOG-1.2.md
@@ -7,3 +7,7 @@
* A new `getSourceDirectory()` method was added on the AssetInterface
* added CssUtils::filterCommentless()
* [BC BREAK] Removed limit and count arguments from CssUtils functions
+ * added `ResourceLoaderInterface`, `DirectoryLoader` and
+ `CoalescingDirectoryLoader`
+ * deprecated `DirectoryResource`, `CoalescingDirectoryResource` and
+ `IteratorResourceInterface`
View
4 src/Assetic/Factory/Loader/CachedFormulaLoader.php
@@ -21,6 +21,7 @@
* A cached formula loader is a composition of a formula loader and a cache.
*
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
*/
class CachedFormulaLoader implements FormulaLoaderInterface
{
@@ -47,6 +48,9 @@ public function __construct(FormulaLoaderInterface $loader, ConfigCache $configC
public function load(ResourceInterface $resources)
{
+ // The IteratorResourceInterface is deprecated
+ // You should not pass resource collections to this class. Pass
+ // single resources instead.
if (!$resources instanceof IteratorResourceInterface) {
$resources = array($resources);
}
View
4 src/Assetic/Factory/Resource/CoalescingDirectoryResource.php
@@ -15,6 +15,10 @@
* Coalesces multiple directories together into one merged resource.
*
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since Assetic 1.2. Use the
+ * {@link Loader\CoalescingDirectoryLoader} instead.
*/
class CoalescingDirectoryResource implements IteratorResourceInterface
{
View
68 src/Assetic/Factory/Resource/DirectoryResource.php
@@ -11,10 +11,16 @@
namespace Assetic\Factory\Resource;
+use Assetic\Factory\Resource\Loader\DirectoryLoader;
+
/**
* A resource is something formulae can be loaded from.
*
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since Assetic 1.2. Use the
+ * {@link Loader\DirectoryLoader} instead.
*/
class DirectoryResource implements IteratorResourceInterface
{
@@ -70,64 +76,20 @@ public function __toString()
return $this->path;
}
- public function getIterator()
- {
- return is_dir($this->path)
- ? new DirectoryResourceIterator($this->getInnerIterator())
- : new \EmptyIterator();
- }
-
- protected function getInnerIterator()
- {
- return new DirectoryResourceFilterIterator(new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern);
- }
-}
-
-/**
- * An iterator that converts file objects into file resources.
- *
- * @author Kris Wallsmith <kris.wallsmith@gmail.com>
- * @access private
- */
-class DirectoryResourceIterator extends \RecursiveIteratorIterator
-{
- public function current()
- {
- return new FileResource(parent::current()->getPathname());
- }
-}
-
-/**
- * Filters files by a basename pattern.
- *
- * @author Kris Wallsmith <kris.wallsmith@gmail.com>
- * @access private
- */
-class DirectoryResourceFilterIterator extends \RecursiveFilterIterator
-{
- protected $pattern;
-
- public function __construct(\RecursiveDirectoryIterator $iterator, $pattern = null)
+ public function getPattern()
{
- parent::__construct($iterator);
-
- $this->pattern = $pattern;
+ return $this->pattern;
}
- public function accept()
+ public function getIterator()
{
- $file = $this->current();
- $name = $file->getBasename();
+ try {
+ $loader = new DirectoryLoader();
- if ($file->isDir()) {
- return '.' != $name[0];
+ return new \ArrayIterator($loader->load($this->path, $this->pattern));
+ } catch (\InvalidArgumentException $exception) {
+ // Ignore non-existing directories
+ return new \EmptyIterator();
}
-
- return null === $this->pattern || 0 < preg_match($this->pattern, $name);
- }
-
- public function getChildren()
- {
- return new self(new \RecursiveDirectoryIterator($this->current()->getPathname(), \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern);
}
}
View
4 src/Assetic/Factory/Resource/IteratorResourceInterface.php
@@ -15,6 +15,10 @@
* A resource is something formulae can be loaded from.
*
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since Assetic 1.2. Use the
+ * {@link Loader\ResourceLoaderInterface} instead.
*/
interface IteratorResourceInterface extends ResourceInterface, \IteratorAggregate
{
View
74 src/Assetic/Factory/Resource/Loader/CoalescingDirectoryLoader.php
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2014 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Factory\Resource\Loader;
+
+/**
+ * Loads resources from multiple directories.
+ *
+ * Use this loader if you want to override resources from one directory in
+ * another directory.
+ *
+ * @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see load()
+ */
+class CoalescingDirectoryLoader
+{
+ /**
+ * Loads resources from a list of directories.
+ *
+ * If two files in two different directories have the same relative path
+ * to their directory, the file of the latter directory overrides the one
+ * in the former. For example, take that the following files exist:
+ *
+ * - /acme/blog/views/layout.html.twig
+ * - /app/views/layout.html.twig
+ *
+ * If you load the following directories:
+ *
+ * $loader = new CoalescingDirectoryLoader();
+ * $resources = $loader->load(array(
+ * '/acme/blog',
+ * '/app'.
+ * ));
+ *
+ * Then only the layout.html.twig file of the "/app" directory is
+ * returned by the loader.
+ *
+ * You can optionally filter the returned resources by passing a regular
+ * expression for the file name in the $pattern argument.
+ *
+ * @param array $directories A list of directory paths
+ * @param string|null $pattern A regular expression or null if you
+ * don't want to filter resources
+ *
+ * @return \Assetic\Factory\Resource\FileResource[] An array of file resources
+ *
+ * @throws \InvalidArgumentException If any of the directories does not exist
+ */
+ public function load($directories, $pattern = null)
+ {
+ $directoryLoader = new DirectoryLoader();
+ $resources = array();
+
+ foreach ($directories as $directory) {
+ $resources = array_replace(
+ $directoryLoader->loadByRelativePath($directory, $pattern),
+ // Directories listed earlier in the array take precedence
+ $resources
+ );
+ }
+
+ return array_values($resources);
+ }
+}
View
113 src/Assetic/Factory/Resource/Loader/DirectoryLoader.php
@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2014 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Factory\Resource\Loader;
+
+use Assetic\Factory\Resource\FileResource;
+use Assetic\Factory\Resource\Util\DirectoryFilterIterator;
+
+/**
+ * Loads resources from a directory.
+ *
+ * @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see load()
+ */
+class DirectoryLoader
+{
+ /**
+ * Loads resources from a directory.
+ *
+ * You can optionally filter the returned resources by passing a regular
+ * expression for the file name in the $pattern argument.
+ *
+ * @param string $directory A directory path
+ * @param string|null $pattern A regular expression or null if you
+ * don't want to filter resources
+ *
+ * @return FileResource[] An array of file resources
+ *
+ * @throws \InvalidArgumentException If the directory does not exist
+ */
+ public function load($directory, $pattern = null)
+ {
+ return array_values($this->loadByRelativePath($directory, $pattern));
+ }
+
+ /**
+ * Loads resources and returns them indexed by their relative path.
+ *
+ * You can optionally filter the returned resources by passing a regular
+ * expression for the file name in the $pattern argument.
+ *
+ * @param string $directory A directory path
+ * @param string|null $pattern A regular expression or null if you
+ * don't want to filter resources
+ *
+ * @return FileResource[] An array of file resources indexed by their
+ * relative path to the directory
+ *
+ * @throws \InvalidArgumentException If the directory does not exist
+ */
+ public function loadByRelativePath($directory, $pattern = null)
+ {
+ if (!is_dir($directory)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'The directory "%s" does not exist.',
+ $directory
+ ));
+ }
+
+ // Append directory separator to get the right relative paths
+ if (DIRECTORY_SEPARATOR != substr($directory, -1)) {
+ $directory .= DIRECTORY_SEPARATOR;
+ }
+
+ $resources = array();
+
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator(
+ $directory,
+ \RecursiveDirectoryIterator::FOLLOW_SYMLINKS
+ | \RecursiveDirectoryIterator::SKIP_DOTS
+ )
+ );
+
+ if (null !== $pattern) {
+ $iterator = new DirectoryFilterIterator($iterator, (string) $pattern);
+ }
+
+ foreach ($iterator as $file) {
+ $file = (string) $file;
+ $relativeName = $this->getRelativePath($file, $directory);
+
+ if (!isset($resources[$relativeName])) {
+ $resources[$relativeName] = new FileResource($file);
+ }
+ }
+
+ return $resources;
+ }
+
+ /**
+ * Returns the relative version of a filename.
+ *
+ * @param string $file The file path
+ * @param string $directory The directory path
+ *
+ * @return string The relative path from the directory to the file
+ */
+ protected function getRelativePath($file, $directory)
+ {
+ return substr($file, strlen($directory));
+ }
+}
View
31 src/Assetic/Factory/Resource/Loader/ResourceLoaderInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2014 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Factory\Resource\Loader;
+
+/**
+ * Loads resources from some source.
+ *
+ * The implementation should decide which sources are acceptable.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ResourceLoaderInterface
+{
+ /**
+ * Loads a list of resources from a source.
+ *
+ * @param mixed $source The source to load the resources from
+ *
+ * @return \Assetic\Factory\Resource\ResourceInterface[] The resources
+ */
+ public function load($source);
+}
View
50 src/Assetic/Factory/Resource/Util/DirectoryFilterIterator.php
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Factory\Resource\Util;
+
+/**
+ * Filters a recursive directory iterator by file name.
+ *
+ * @author Kris Wallsmith <kris.wallsmith@gmail.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class DirectoryFilterIterator extends \FilterIterator
+{
+ /**
+ * @var string
+ */
+ private $pattern;
+
+ /**
+ * Creates a new filter iterator based on a recursive directory iterator.
+ *
+ * @param \Iterator $iterator The inner iterator
+ * @param string $pattern A regular expression
+ *
+ * @throws \InvalidArgumentException If the pattern is not a string
+ */
+ public function __construct(\Iterator $iterator, $pattern)
+ {
+ parent::__construct($iterator);
+
+ if (!is_string($pattern)) {
+ throw new \InvalidArgumentException('The pattern should be a string');
+ }
+
+ $this->pattern = $pattern;
+ }
+
+ public function accept()
+ {
+ return preg_match($this->pattern, $this->current()->getBasename());
+ }
+}
View
52 tests/Assetic/Test/Factory/Resource/Loader/CoalescingDirectoryLoaderTest.php
@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2014 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Test\Factory\Resource\Loader;
+
+use Assetic\Factory\Resource\Loader\CoalescingDirectoryLoader;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CoalescingDirectoryLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var CoalescingDirectoryLoader
+ */
+ private $loader;
+
+ protected function setUp()
+ {
+ $this->loader = new CoalescingDirectoryLoader();
+ }
+
+ public function testLoad()
+ {
+ // notice only one directory has a trailing slash
+ $resources = $this->loader->load(array(
+ __DIR__.'/../Fixtures/dir1/',
+ __DIR__.'/../Fixtures/dir2',
+ ), '/\.txt$/');
+
+ $paths = array();
+ foreach ($resources as $resource) {
+ $paths[] = realpath((string) $resource);
+ }
+ sort($paths);
+
+ $this->assertEquals(array(
+ realpath(__DIR__.'/../Fixtures/dir1/file1.txt'),
+ realpath(__DIR__.'/../Fixtures/dir1/file2.txt'),
+ realpath(__DIR__.'/../Fixtures/dir2/file3.txt'),
+ ), $paths, 'files from multiple directories are merged');
+ }
+
+}
View
117 tests/Assetic/Test/Factory/Resource/Loader/DirectoryLoaderTest.php
@@ -0,0 +1,117 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2014 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Test\Factory\Resource\Loader;
+
+use Assetic\Factory\Resource\Loader\DirectoryLoader;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class DirectoryLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var DirectoryLoader
+ */
+ private $loader;
+
+ protected function setUp()
+ {
+ $this->loader = new DirectoryLoader();
+ }
+
+ /**
+ * @dataProvider getPatternsAndEmpty
+ */
+ public function testLoad($pattern, $empty)
+ {
+ $resources = $this->loader->load(__DIR__.'/..', $pattern);
+
+ foreach ($resources as $resource) {
+ $this->assertInstanceOf('Assetic\\Factory\\Resource\\ResourceInterface', $resource);
+ }
+
+ if ($empty) {
+ $this->assertCount(0, $resources);
+ } else {
+ $this->assertGreaterThan(0, count($resources));
+ }
+ }
+
+ public function testLoadByRelativePath()
+ {
+ $resources = $this->loader->loadByRelativePath(__DIR__.'/../Fixtures');
+
+ $values = $this->loader->load(__DIR__.'/../Fixtures');
+
+ $this->assertEquals($values, array_values($resources));
+
+ $keys = array(
+ 'css/style.css',
+ 'dir1/file1.txt',
+ 'dir1/file2.txt',
+ 'dir2/file1.txt',
+ 'dir2/file3.txt',
+ );
+
+ // Order is not determined
+ ksort($resources);
+
+ $this->assertEquals($keys, array_keys($resources));
+ }
+
+ public function getPatternsAndEmpty()
+ {
+ return array(
+ array(null, false),
+ array('/\.php$/', false),
+ array('/\.foo$/', true),
+ );
+ }
+
+ public function testLoadRecursively()
+ {
+ $resources = $this->loader->load(realpath(__DIR__.'/..'), '/^'.preg_quote(basename(__FILE__)).'$/');
+
+ $this->assertCount(1, $resources);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testInvalidDirectory()
+ {
+ $this->loader->load(__DIR__.'foo');
+ }
+
+ public function testFollowSymlinks()
+ {
+ // Create the symlink if it doesn't already exist yet (if someone broke the entire testsuite perhaps)
+ if (!is_dir(__DIR__.'/../Fixtures/dir3')) {
+ symlink(__DIR__.'/../Fixtures/dir2', __DIR__.'/../Fixtures/dir3');
+ }
+
+ $resources = $this->loader->load(__DIR__.'/../Fixtures');
+
+ $this->assertCount(7, $resources);
+ }
+
+ protected function tearDown()
+ {
+ if (is_dir(__DIR__.'/../Fixtures/dir3') && is_link(__DIR__.'/../Fixtures/dir3')) {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ rmdir(__DIR__.'/../Fixtures/dir3');
+ } else {
+ unlink(__DIR__.'/../Fixtures/dir3');
+ }
+ }
+ }
+}
Something went wrong with that request. Please try again.