Skip to content

Commit e7c00fa

Browse files
author
Bastian Waidelich
committed
[BUGFIX] Fix PSR-4 support for Flow packages
Basic support for ``PSR-4`` structured packages has been added with Flow 2.2 (see I9b2dae7761ef48389d9915c1269df2fdf771af8c). However, automatic re-reflection didn't work for PSR-4 Flow packages. Besides, custom ``Package.php`` files were expected to be located at ``Acme.PackageKey/Classes/Acme/PackageKey/Package.php`` which is not in sync with PSR-4. This change fixes the two issues by extracting the actual class name from the actual PHP files instead of trying to defer it from the file path. Besides it replaces the static lookup path for custom ``Package.php`` files taking the ``autoload`` property of the composer manifest into account. Note: While this fixes basic support for PSR-4 Flow still doesn't support all composer features. Primarily there is a 1:1 mapping from package to autoloading type and classes path while composer manifests support multiple mappings by namespace. Change-Id: I9c613df54a8b650c53b4ab8e03071432e13d3c4e Releases: master, 3.0 Related: FLOW-238 Original-Commit-Hash: c1612b81a46ca38f71aa15e3267baec04d36ff7c
1 parent 35d3c61 commit e7c00fa

File tree

5 files changed

+253
-40
lines changed

5 files changed

+253
-40
lines changed

TYPO3.Flow/Classes/TYPO3/Flow/Cache/CacheManager.php

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
* */
1313

1414
use TYPO3\Flow\Annotations as Flow;
15+
use TYPO3\Flow\Cache\Backend\BackendInterface;
16+
use TYPO3\Flow\Cache\Backend\TaggableBackendInterface;
1517
use TYPO3\Flow\Cache\Frontend\FrontendInterface;
18+
use TYPO3\Flow\Utility\PhpAnalyzer;
1619

1720
/**
1821
* The Cache Manager
@@ -156,6 +159,7 @@ public function hasCache($identifier) {
156159
*/
157160
public function flushCaches() {
158161
$this->createAllCaches();
162+
/** @var BackendInterface $cache */
159163
foreach ($this->caches as $cache) {
160164
$cache->flush();
161165
}
@@ -172,6 +176,7 @@ public function flushCaches() {
172176
*/
173177
public function flushCachesByTag($tag) {
174178
$this->createAllCaches();
179+
/** @var TaggableBackendInterface $cache */
175180
foreach ($this->caches as $cache) {
176181
$cache->flushByTag($tag);
177182
}
@@ -228,26 +233,24 @@ protected function flushClassCachesByChangedFiles(array $changedFiles) {
228233
$modifiedAspectClassNamesWithUnderscores = array();
229234
$modifiedClassNamesWithUnderscores = array();
230235
foreach ($changedFiles as $pathAndFilename => $status) {
231-
$pathAndFilename = str_replace(FLOW_PATH_PACKAGES, '', $pathAndFilename);
232-
$matches = array();
233-
// safeguard against projects having illegal filenames below "Classes" (like phpexcel/phpexcel, see https://phpexcel.codeplex.com/workitem/20336)
234-
if (preg_match('/[^\/]+\/(.+)\/(Classes|Tests)\/([^.]+)\.php/', $pathAndFilename, $matches) === 1) {
235-
if ($matches[2] === 'Classes') {
236-
$classNameWithUnderscores = str_replace('/', '_', $matches[3]);
237-
} else {
238-
$classNameWithUnderscores = str_replace('/', '_', $matches[1] . '_' . ($matches[2] === 'Tests' ? 'Tests_' : '') . $matches[3]);
239-
$classNameWithUnderscores = str_replace('.', '_', $classNameWithUnderscores);
240-
}
241-
$modifiedClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
236+
if (!file_exists($pathAndFilename)) {
237+
continue;
238+
}
239+
$fileContents = file_get_contents($pathAndFilename);
240+
$className = (new PhpAnalyzer($fileContents))->extractFullyQualifiedClassName();
241+
if ($className === NULL) {
242+
continue;
243+
}
244+
$classNameWithUnderscores = str_replace('\\', '_', $className);
245+
$modifiedClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
242246

243-
// If an aspect was modified, the whole code cache needs to be flushed, so keep track of them:
244-
if (substr($classNameWithUnderscores, -6, 6) === 'Aspect') {
245-
$modifiedAspectClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
246-
}
247-
// As long as no modified aspect was found, we are optimistic that only part of the cache needs to be flushed:
248-
if (count($modifiedAspectClassNamesWithUnderscores) === 0) {
249-
$objectClassesCache->remove($classNameWithUnderscores);
250-
}
247+
// If an aspect was modified, the whole code cache needs to be flushed, so keep track of them:
248+
if (substr($classNameWithUnderscores, -6, 6) === 'Aspect') {
249+
$modifiedAspectClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
250+
}
251+
// As long as no modified aspect was found, we are optimistic that only part of the cache needs to be flushed:
252+
if (count($modifiedAspectClassNamesWithUnderscores) === 0) {
253+
$objectClassesCache->remove($classNameWithUnderscores);
251254
}
252255
}
253256
$flushDoctrineProxyCache = FALSE;

TYPO3.Flow/Classes/TYPO3/Flow/Package/PackageFactory.php

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
* The TYPO3 project - inspiring people to share! *
1212
* */
1313

14+
use TYPO3\Flow\Package\Exception\MissingPackageManifestException;
1415
use TYPO3\Flow\Utility\Files;
16+
use TYPO3\Flow\Utility\PhpAnalyzer;
1517

1618
/**
1719
* Class for building Packages
@@ -24,8 +26,6 @@ class PackageFactory {
2426
protected $packageManager;
2527

2628
/**
27-
* Constructor
28-
*
2929
* @param \TYPO3\Flow\Package\PackageManagerInterface $packageManager
3030
*/
3131
public function __construct(PackageManagerInterface $packageManager) {
@@ -43,26 +43,34 @@ public function __construct(PackageManagerInterface $packageManager) {
4343
* @return \TYPO3\Flow\Package\PackageInterface
4444
* @throws Exception\CorruptPackageException
4545
*/
46-
public function create($packagesBasePath, $packagePath, $packageKey, $classesPath, $manifestPath = '') {
47-
$packageClassPathAndFilename = Files::concatenatePaths(array($packagesBasePath, $packagePath, 'Classes/' . str_replace('.', '/', $packageKey) . '/Package.php'));
46+
public function create($packagesBasePath, $packagePath, $packageKey, $classesPath = NULL, $manifestPath = NULL) {
47+
$absolutePackagePath = Files::concatenatePaths(array($packagesBasePath, $packagePath)) . '/';
48+
$absoluteManifestPath = Files::concatenatePaths(array($absolutePackagePath, $manifestPath)) . '/';
49+
$autoLoadDirectives = array();
50+
try {
51+
$autoLoadDirectives = (array)PackageManager::getComposerManifest($absoluteManifestPath, 'autoload');
52+
} catch (MissingPackageManifestException $exception) {
53+
}
54+
if (isset($autoLoadDirectives[Package::AUTOLOADER_TYPE_PSR4])) {
55+
$packageClassPathAndFilename = Files::concatenatePaths(array($packagesBasePath, $packagePath, 'Classes', 'Package.php'));
56+
} else {
57+
$packageClassPathAndFilename = Files::concatenatePaths(array($packagesBasePath, $packagePath, 'Classes', str_replace('.', '/', $packageKey), 'Package.php'));
58+
}
59+
$package = NULL;
4860
if (file_exists($packageClassPathAndFilename)) {
4961
require_once($packageClassPathAndFilename);
50-
/**
51-
* @todo there should be a general method for getting Namespace from $packageKey
52-
* @todo it should be tested if the package class implements the interface
53-
*/
54-
$packageClassName = str_replace('.', '\\', $packageKey) . '\Package';
55-
if (!class_exists($packageClassName)) {
56-
throw new \TYPO3\Flow\Package\Exception\CorruptPackageException(sprintf('The package "%s" does not contain a valid package class. Check if the file "%s" really contains a class called "%s".', $packageKey, $packageClassPathAndFilename, $packageClassName), 1327587091);
62+
$packageClassContents = file_get_contents($packageClassPathAndFilename);
63+
$packageClassName = (new PhpAnalyzer($packageClassContents))->extractFullyQualifiedClassName();
64+
if ($packageClassName === NULL) {
65+
throw new Exception\CorruptPackageException(sprintf('The package "%s" does not contain a valid package class. Check if the file "%s" really contains a class.', $packageKey, $packageClassPathAndFilename), 1327587091);
5766
}
58-
} else {
59-
$packageClassName = 'TYPO3\Flow\Package\Package';
67+
$package = new $packageClassName($this->packageManager, $packageKey, $absolutePackagePath, $classesPath, $manifestPath);
68+
if (!$package instanceof PackageInterface) {
69+
throw new Exception\CorruptPackageException(sprintf('The package class of package "%s" does not implement \TYPO3\Flow\Package\PackageInterface. Check the file "%s".', $packageKey, $packageClassPathAndFilename), 1427193370);
70+
}
71+
return $package;
6072
}
61-
$packagePath = Files::concatenatePaths(array($packagesBasePath, $packagePath)) . '/';
62-
63-
$package = new $packageClassName($this->packageManager, $packageKey, $packagePath, $classesPath, $manifestPath);
64-
65-
return $package;
73+
return new Package($this->packageManager, $packageKey, $absolutePackagePath, $classesPath, $manifestPath);
6674
}
6775

6876
/**
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
namespace TYPO3\Flow\Utility;
3+
4+
/* *
5+
* This script belongs to the TYPO3 Flow framework. *
6+
* *
7+
* It is free software; you can redistribute it and/or modify it under *
8+
* the terms of the GNU Lesser General Public License, either version 3 *
9+
* of the License, or (at your option) any later version. *
10+
* *
11+
* The TYPO3 project - inspiring people to share! *
12+
* */
13+
14+
/**
15+
* This utility class can be used to extract information about PHP files without having to instantiate/reflect classes.
16+
*
17+
* Usage:
18+
*
19+
* // extract the FQN e.g. "Some\Namespace\SomeClassName"
20+
* $fullyQualifiedClassName = (new PhpAnalyzer($fileContents))->extractFullyQualifiedClassName();
21+
*
22+
* // extract the namespace "Some\Namespace"
23+
* $namespace = (new PhpAnalyzer($fileContents))->extractNamespace();
24+
*
25+
* // extract just the class name "SomeClassName"
26+
* $className = (new PhpAnalyzer($fileContents))->extractClassName();
27+
*/
28+
class PhpAnalyzer {
29+
30+
/**
31+
* @var string
32+
*/
33+
protected $phpCode;
34+
35+
/**
36+
* @param string $phpCode
37+
*/
38+
public function __construct($phpCode) {
39+
$this->phpCode = $phpCode;
40+
}
41+
42+
/**
43+
* Extracts the Fully Qualified Class name from the given PHP code
44+
*
45+
* @return string FQN in the format "Some\Fully\Qualified\ClassName" or NULL if no class was detected
46+
*/
47+
public function extractFullyQualifiedClassName() {
48+
$fullyQualifiedClassName = $this->extractClassName();
49+
if ($fullyQualifiedClassName === NULL) {
50+
return NULL;
51+
}
52+
$namespace = $this->extractNamespace();
53+
if ($namespace !== NULL) {
54+
$fullyQualifiedClassName = $namespace . '\\' . $fullyQualifiedClassName;
55+
}
56+
return $fullyQualifiedClassName;
57+
}
58+
59+
/**
60+
* Extracts the PHP namespace from the given PHP code
61+
*
62+
* @return string the PHP namespace in the form "Some\Namespace" (w/o leading backslash) - or NULL if no namespace modifier was found
63+
*/
64+
public function extractNamespace() {
65+
$namespaceParts = array();
66+
$tokens = token_get_all($this->phpCode);
67+
$numberOfTokens = count($tokens);
68+
for ($i = 0; $i < $numberOfTokens; $i++) {
69+
$token = $tokens[$i];
70+
if (is_string($token) || $token[0] !== T_NAMESPACE) {
71+
continue;
72+
}
73+
for (++$i; $i < $numberOfTokens; $i++) {
74+
$token = $tokens[$i];
75+
if (is_string($token)) {
76+
break;
77+
}
78+
list($type, $value) = $token;
79+
if ($type === T_STRING) {
80+
$namespaceParts[] = $value;
81+
continue;
82+
}
83+
if ($type !== T_NS_SEPARATOR && $type !== T_WHITESPACE) {
84+
break;
85+
}
86+
}
87+
break;
88+
}
89+
if ($namespaceParts === array()) {
90+
return NULL;
91+
}
92+
return implode('\\', $namespaceParts);
93+
}
94+
95+
/**
96+
* Extracts the className of the given PHP code
97+
* Note: This only returns the class name without namespace, @see extractFullyQualifiedClassName()
98+
*
99+
* @return string
100+
*/
101+
public function extractClassName() {
102+
$tokens = token_get_all($this->phpCode);
103+
$numberOfTokens = count($tokens);
104+
for ($i = 0; $i < $numberOfTokens; $i++) {
105+
$token = $tokens[$i];
106+
if (is_string($token) || $token[0] !== T_CLASS) {
107+
continue;
108+
}
109+
for (++$i; $i < $numberOfTokens; $i++) {
110+
$token = $tokens[$i];
111+
if (is_string($token)) {
112+
break;
113+
}
114+
list($type, $value) = $token;
115+
if ($type === T_STRING) {
116+
return $value;
117+
}
118+
if ($type !== T_WHITESPACE) {
119+
break;
120+
}
121+
}
122+
}
123+
return NULL;
124+
}
125+
126+
}

TYPO3.Flow/Tests/Unit/Cache/CacheManagerTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public function flushSystemCachesByChangedFilesWithChangedClassFileRemovesCacheE
167167
$objectConfigurationCache->expects($this->once())->method('remove')->with('allCompiledCodeUpToDate');
168168

169169
$this->cacheManager->flushSystemCachesByChangedFiles('Flow_ClassFiles', array(
170-
FLOW_PATH_PACKAGES . '/Framework/TYPO3.Flow/Classes/TYPO3/Flow/Cache/CacheManager.php' => ChangeDetectionStrategyInterface::STATUS_CHANGED
170+
FLOW_PATH_PACKAGES . 'Framework/TYPO3.Flow/Classes/TYPO3/Flow/Cache/CacheManager.php' => ChangeDetectionStrategyInterface::STATUS_CHANGED
171171
));
172172
}
173173

@@ -179,11 +179,11 @@ public function flushSystemCachesByChangedFilesWithChangedTestFileRemovesCacheEn
179179
$objectConfigurationCache = $this->registerCache('Flow_Object_Configuration');
180180
$this->registerCache('Flow_Reflection_Status');
181181

182-
$objectClassCache->expects($this->once())->method('remove')->with('TYPO3_Flow_Tests_Functional_Cache_CacheManagerTest');
182+
$objectClassCache->expects($this->once())->method('remove')->with('TYPO3_Flow_Tests_Unit_Cache_CacheManagerTest');
183183
$objectConfigurationCache->expects($this->once())->method('remove')->with('allCompiledCodeUpToDate');
184184

185185
$this->cacheManager->flushSystemCachesByChangedFiles('Flow_ClassFiles', array(
186-
FLOW_PATH_PACKAGES . '/Framework/TYPO3.Flow/Tests/Functional/Cache/CacheManagerTest.php' => ChangeDetectionStrategyInterface::STATUS_CHANGED
186+
__FILE__ => ChangeDetectionStrategyInterface::STATUS_CHANGED
187187
));
188188
}
189189

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
namespace TYPO3\Flow\Tests\Unit\Utility;
3+
4+
/* *
5+
* This script belongs to the TYPO3 Flow framework. *
6+
* *
7+
* It is free software; you can redistribute it and/or modify it under *
8+
* the terms of the GNU Lesser General Public License, either version 3 *
9+
* of the License, or (at your option) any later version. *
10+
* *
11+
* The TYPO3 project - inspiring people to share! *
12+
* */
13+
14+
use TYPO3\Flow\Tests\UnitTestCase;
15+
use TYPO3\Flow\Utility\PhpAnalyzer;
16+
17+
/**
18+
* Testcase for the PhpAnalyzer utility class
19+
*/
20+
class PhpAnalyzerTest extends UnitTestCase {
21+
22+
/**
23+
* @return array
24+
*/
25+
public function sampleClasses() {
26+
return array(
27+
array('phpCode' => '', 'namespace' => NULL, 'className' => NULL, 'fqn' => NULL),
28+
array('phpCode' => 'namespace Foo;', 'namespace' => NULL, 'className' => NULL, 'fqn' => NULL),
29+
array('phpCode' => 'class Bar {}', 'namespace' => NULL, 'className' => NULL, 'fqn' => NULL),
30+
array('phpCode' => '<?php class {}', 'namespace' => NULL, 'className' => NULL, 'fqn' => NULL),
31+
32+
array('phpCode' => '<?php class SomeClass {}', 'namespace' => NULL, 'className' => 'SomeClass', 'fqn' => 'SomeClass'),
33+
array('phpCode' => '<?php namespace Foo\Bar; class SomeClass {}', 'namespace' => 'Foo\Bar', 'className' => 'SomeClass', 'fqn' => 'Foo\Bar\SomeClass'),
34+
35+
array('phpCode' => '<?php namespace \Foo\Bar\; class SomeClass {}', 'namespace' => 'Foo\Bar', 'className' => 'SomeClass', 'fqn' => 'Foo\Bar\SomeClass'),
36+
array('phpCode' => '<?php ' . chr(13) . ' namespace Foo\Bar {' . chr(13) . ' class SomeClass {}', 'namespace' => 'Foo\Bar', 'className' => 'SomeClass', 'fqn' => 'Foo\Bar\SomeClass'),
37+
array('phpCode' => 'foo <?php class SomeClass', 'namespace' => NULL, 'className' => 'SomeClass', 'fqn' => 'SomeClass'),
38+
);
39+
}
40+
41+
/**
42+
* @param string $phpCode
43+
* @param string $namespace
44+
* @test
45+
* @dataProvider sampleClasses
46+
*/
47+
public function extractNamespaceTests($phpCode, $namespace) {
48+
$phpAnalyzer = new PhpAnalyzer($phpCode);
49+
$this->assertSame($namespace, $phpAnalyzer->extractNamespace());
50+
}
51+
52+
/**
53+
* @param string $phpCode
54+
* @param string $namespace
55+
* @param string $className
56+
* @test
57+
* @dataProvider sampleClasses
58+
*/
59+
public function extractClassNameTests($phpCode, $namespace, $className) {
60+
$phpAnalyzer = new PhpAnalyzer($phpCode);
61+
$this->assertSame($className, $phpAnalyzer->extractClassName());
62+
}
63+
64+
/**
65+
* @param string $phpCode
66+
* @param string $namespace
67+
* @param string $className
68+
* @param string $fqn
69+
* @test
70+
* @dataProvider sampleClasses
71+
*/
72+
public function extractFullyQualifiedClassNameTests($phpCode, $namespace, $className, $fqn) {
73+
$phpAnalyzer = new PhpAnalyzer($phpCode);
74+
$this->assertSame($fqn, $phpAnalyzer->extractFullyQualifiedClassName());
75+
}
76+
}

0 commit comments

Comments
 (0)