Skip to content

Commit

Permalink
Merge pull request #35 from sitegeist/feature/componentLoader
Browse files Browse the repository at this point in the history
Feature/component loader
  • Loading branch information
s2b authored Aug 27, 2019
2 parents e3dafcc + d53c9c6 commit 230c2a5
Show file tree
Hide file tree
Showing 14 changed files with 329 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ before_script:
- composer install --prefer-dist
script:
- composer lint
#- composer test
- composer test
120 changes: 110 additions & 10 deletions Classes/Utility/ComponentLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public function __construct()
*/
public function addNamespace(string $namespace, string $path): self
{
// Sanitize namespace data
$namespace = $this->sanitizeNamespace($namespace);
$path = $this->sanitizePath($path);

$this->namespaces[$namespace] = $path;
return $this;
}
Expand All @@ -61,9 +65,15 @@ public function removeNamespace(string $namespace): self
*/
public function setNamespaces(array $namespaces): self
{
// Make sure that namespaces are sanitized
$this->namespaces = [];
foreach ($namespaces as $namespace => $path) {
$this->addNamespace($namespace, $path);
}

// Order by namespace specificity
arsort($namespaces);
$this->namespaces = $namespaces;
krsort($this->namespaces);

return $this;
}

Expand All @@ -87,32 +97,122 @@ public function getNamespaces(): array
public function findComponent(string $class, string $ext = '.html')
{
// Try cache first
if (isset($this->componentsCache[$class])) {
return $this->componentsCache[$class] . $ext;
$cacheIdentifier = $class . '|' . $ext;
if (isset($this->componentsCache[$cacheIdentifier])) {
return $this->componentsCache[$cacheIdentifier];
}

// Walk through available namespaces, ordered from specific to unspecific
$class = ltrim($class, '\\');
foreach ($this->namespaces as $namespace => $path) {
$namespace = ltrim($namespace, '\\');

// No match, skip to next
if (strpos($class, $namespace) !== 0) {
if (strpos($class, $namespace . '\\') !== 0) {
continue;
}

$componentParts = explode('\\', trim(substr($class, strlen($namespace)), '\\'));

$componentPath = rtrim($path, '/') . '/' . implode('/', $componentParts) . '/' . end($componentParts);
$componentFile = $componentPath . $ext;
$componentPath = $path . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $componentParts);
$componentFile = $componentPath . DIRECTORY_SEPARATOR . end($componentParts) . $ext;

// Check if component file exists
if (file_exists($componentFile)) {
$this->componentsCache[$class] = $componentPath;
$this->componentsCache[$cacheIdentifier] = $componentFile;
return $componentFile;
}
}

return null;
}

/**
* Provides a list of all components that are available in the specified component namespace
*
* @param string $namespace
* @param string $ext
* @return array Array of components where the keys contain the component identifier (FQCN)
* and the values contain the path to the component
*/
public function findComponentsInNamespace(string $namespace, string $ext = '.html'): array
{
if (!isset($this->namespaces[$namespace])) {
return [];
}

$scannedPaths = [];
return $this->scanForComponents(
$this->namespaces[$namespace],
$ext,
$namespace,
$scannedPaths
);
}

/**
* Searches for component files in a directory and maps them to their namespace
*
* @param string $path
* @param string $ext
* @param string $namespace
* @param array $scannedPaths Collection of paths that have already been scanned for components;
* this prevents infinite loops caused by circular symlinks
* @return array
*/
protected function scanForComponents(string $path, string $ext, string $namespace, array &$scannedPaths): array
{
$components = [];

$componentCandidates = scandir($path);
foreach ($componentCandidates as $componentName) {
// Skip relative links
if ($componentName === '.' || $componentName === '..') {
continue;
}

// Only search for directories and prevent infinite loops
$componentPath = realpath($path . DIRECTORY_SEPARATOR . $componentName);
if (!is_dir($componentPath) || isset($scannedPaths[$componentPath])) {
continue;
}
$scannedPaths[$componentPath] = true;

$componentNamespace = $namespace . '\\' . $componentName;
$componentFile = $componentPath . DIRECTORY_SEPARATOR . $componentName . $ext;

// Only match folders that contain a component file
if (file_exists($componentFile)) {
$components[$componentNamespace] = $componentFile;
}

// Continue recursively
$components = array_merge(
$components,
$this->scanForComponents($componentPath, $ext, $componentNamespace, $scannedPaths)
);
}

return $components;
}

/**
* Sanitizes a PHP namespace for use in the component loader
*
* @param string $namespace
* @return string
*/
protected function sanitizeNamespace(string $namespace): string
{
return trim($namespace, '\\');
}

/**
* Sanitizes a path for use in the component loader
*
* @param string $path
* @return string
*/
protected function sanitizePath(string $path): string
{
return rtrim($path, DIRECTORY_SEPARATOR);
}
}
Empty file.
Empty file.
1 change: 1 addition & 0 deletions Tests/Fixtures/ComponentLoader/Atom/ComponentLoaderSymlink
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions Tests/Fixtures/ComponentLoader/Molecule/Circular
1 change: 1 addition & 0 deletions Tests/Fixtures/ComponentLoader/Molecule/Example
Empty file.
Empty file.
Empty file.
215 changes: 215 additions & 0 deletions Tests/Unit/ComponentLoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?php

namespace SMS\FluidComponents\Tests\Unit;

use SMS\FluidComponents\Utility\ComponentLoader;

class ComponentLoaderTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
{
protected function setUp()
{
parent::setUp();

$this->loader = new ComponentLoader();
}

protected function getFixturePath($fixtureName)
{
return realpath(dirname(__FILE__) . '/../Fixtures/' . $fixtureName);
}

public function addNamespaceProvider()
{
return [
'namespaceWithLeadingTrailingBackslash' => [
'\\Vendor\\Extension\\Category\\',
'/path/to/components',
[
'Vendor\\Extension\\Category' => '/path/to/components'
]
],
'pathWithTrailingSlash' => [
'Vendor\\Extension\\Category\\',
'/path/to/components/',
[
'Vendor\\Extension\\Category' => '/path/to/components'
]
]
];
}

/**
* @test
* @dataProvider addNamespaceProvider
*/
public function addNamespace(string $namespace, string $path, array $namespaces)
{
$this->loader->addNamespace($namespace, $path);
$this->assertEquals(
$namespaces,
$this->loader->getNamespaces()
);
}

/**
* @test
* @depends addNamespace
*/
public function removeNamespace()
{
$namespace = 'Vendor\\Extension\\Category';
$this->loader
->addNamespace($namespace, 'some/path')
->removeNamespace($namespace);

$this->assertEquals(
[],
$this->loader->getNamespaces()
);
}

public function setNamespacesProvider()
{
return [
'case1' => [
[
'Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path1',
'Sitegeist\\Fixtures\\Component\\Loader' => '/case1/path2',
'Sitegeist\\Fixtures\\ComponentLoader\\Test' => '/case1/path3',
'Vendor\\Test\\Namespace' => '/case1/path4',
'\\Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path5',
'\\Sitegeist\\Fixtures\\ComponentLoader\\' => '/case1/path6',
'\\Sitegeist\\Fixtures\\AnotherTest\\' => '/case1/path7',
'\\Sitegeist\\Fixtures\\Test\\' => '/case1/path8'
],
[
'Vendor\\Test\\Namespace' => '/case1/path4',
'Sitegeist\\Fixtures\\Test' => '/case1/path8',
'Sitegeist\\Fixtures\\AnotherTest' => '/case1/path7',
'Sitegeist\\Fixtures\\ComponentLoader\\Test' => '/case1/path3',
'Sitegeist\\Fixtures\\ComponentLoader' => '/case1/path6',
'Sitegeist\\Fixtures\\Component\\Loader' => '/case1/path2'
],
]
];
}

/**
* @test
* @dataProvider setNamespacesProvider
*/
public function setNamespaces(array $namespaces, array $sortedNamespaces)
{
$this->loader->setNamespaces($namespaces);
$this->assertEquals($sortedNamespaces, $this->loader->getNamespaces());
}

public function findComponentProvider()
{
return [
'existingComponent' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Button',
'.html',
$this->getFixturePath('ComponentLoader/Atom/Button/Button.html')
],
'existingComponentFileExtension' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Button',
'.test',
$this->getFixturePath('ComponentLoader/Atom/Button/Button.test')
],
'existingComponentFirstLevel' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Example',
'.html',
$this->getFixturePath('ComponentLoader/Example/Example.html')
],
'existingComponentThirdLevel' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Molecule\\Teaser\\Headline',
'.html',
$this->getFixturePath('ComponentLoader/Molecule/Teaser/Headline/Headline.html')
],
'nonexistingComponent' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Label',
'.html',
null
],
'nonexistingComponentFileExtension' => [
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Button',
'.nonexisting',
null
],
'nonexistingComponentNamespace' => [
'Sitegeist\\Fixtures\\Nonexisting\\Component',
'.html',
null
],
];
}

/**
* @test
* @depends addNamespace
* @dataProvider findComponentProvider
*/
public function findComponent(string $componentIdentifier, string $fileExtension, $result)
{
$this->loader->addNamespace(
'Sitegeist\\Fixtures\\ComponentLoader',
$this->getFixturePath('ComponentLoader')
);

// Test uncached version
$this->assertEquals(
$result,
$this->loader->findComponent($componentIdentifier, $fileExtension)
);

// Test cached version
$this->assertEquals(
$result,
$this->loader->findComponent($componentIdentifier, $fileExtension)
);
}

public function findComponentsInNamespaceProvider()
{
return [
'html' => [
'.html',
[
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Button' => $this->getFixturePath('ComponentLoader/Atom/Button/Button.html'),
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\ComponentLoaderSymlink' => $this->getFixturePath('ComponentLoader/Atom/ComponentLoaderSymlink/ComponentLoaderSymlink.html'),
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Link' => $this->getFixturePath('ComponentLoader/Atom/Link/Link.html'),
'Sitegeist\\Fixtures\\ComponentLoader\\Example' => $this->getFixturePath('ComponentLoader/Example/Example.html'),
'Sitegeist\\Fixtures\\ComponentLoader\\Molecule\\Teaser' => $this->getFixturePath('ComponentLoader/Molecule/Teaser/Teaser.html'),
'Sitegeist\\Fixtures\\ComponentLoader\\Molecule\\Teaser\\Headline' => $this->getFixturePath('ComponentLoader/Molecule/Teaser/Headline/Headline.html')
]
],
'test' => [
'.test',
[
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Button' => $this->getFixturePath('ComponentLoader/Atom/Button/Button.test'),
'Sitegeist\\Fixtures\\ComponentLoader\\Atom\\Link' => $this->getFixturePath('ComponentLoader/Atom/Link/Link.test')
]
]
];
}

/**
* @test
* @depends addNamespace
* @dataProvider findComponentsInNamespaceProvider
*/
public function findComponentsInNamespace(string $fileExtension, array $result)
{
$namespace = 'Sitegeist\\Fixtures\\ComponentLoader';
$this->loader->addNamespace(
$namespace,
$this->getFixturePath('ComponentLoader')
);

$this->assertEquals(
$result,
$this->loader->findComponentsInNamespace($namespace, $fileExtension)
);
}
}

0 comments on commit 230c2a5

Please sign in to comment.