Skip to content

Commit 9e90cbc

Browse files
committed
Adding support for passing \Absolute\Class\Paths as class names
This also means that the App\ prefix is now hardcoded in exactly *one* place, and could be moved to configuration very easily.
1 parent c9617ba commit 9e90cbc

23 files changed

+297
-81
lines changed

src/Command/MakerCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ protected function interact(InputInterface $input, OutputInterface $output)
8787

8888
protected function execute(InputInterface $input, OutputInterface $output)
8989
{
90-
$generator = new Generator($this->fileManager);
90+
$generator = new Generator($this->fileManager, 'App\\');
9191

9292
$this->maker->generate($input, $this->io, $generator);
9393

src/FileManager.php

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bundle\MakerBundle;
1313

14+
use Composer\Autoload\ClassLoader;
1415
use Symfony\Component\Console\Style\SymfonyStyle;
1516
use Symfony\Component\Filesystem\Filesystem;
1617

@@ -20,17 +21,23 @@
2021
*
2122
* @internal
2223
*/
23-
final class FileManager
24+
class FileManager
2425
{
2526
private $fs;
2627
private $rootDirectory;
2728
/** @var SymfonyStyle */
2829
private $io;
2930

31+
private static $classLoader;
32+
3033
public function __construct(Filesystem $fs, string $rootDirectory)
3134
{
35+
if (!file_exists($rootDirectory)) {
36+
throw new \InvalidArgumentException(sprintf('Root directory "%s" does not exist.', $rootDirectory));
37+
}
38+
3239
$this->fs = $fs;
33-
$this->rootDirectory = $rootDirectory;
40+
$this->rootDirectory = rtrim($this->realpath($rootDirectory).'/');
3441
}
3542

3643
public function setIO(SymfonyStyle $io)
@@ -76,21 +83,62 @@ public function relativizePath($absolutePath): string
7683

7784
public function getPathForFutureClass(string $className)
7885
{
79-
$autoloadPath = $this->absolutizePath('vendor/autoload.php');
86+
// lookup is obviously modeled off of Composer's autoload logic
87+
foreach ($this->getClassLoader()->getPrefixesPsr4() as $prefix => $paths) {
88+
if (0 === strpos($className, $prefix)) {
89+
$path = $paths[0] . '/' . str_replace('\\', '/', str_replace($prefix, '', $className)) . '.php';
90+
91+
return $this->relativizePath($path);
92+
}
93+
}
94+
95+
foreach ($this->getClassLoader()->getPrefixes() as $prefix => $paths) {
96+
if (0 === strpos($className, $prefix)) {
97+
$path = $paths[0] . '/' . str_replace('\\', '/', $className) . '.php';
98+
99+
return $this->relativizePath($path);
100+
}
101+
}
102+
103+
if ($this->getClassLoader()->getFallbackDirsPsr4()) {
104+
$path = $this->getClassLoader()->getFallbackDirsPsr4()[0] . '/' . str_replace('\\', '/', $className) . '.php';
105+
106+
return $this->relativizePath($path);
107+
}
108+
109+
if ($this->getClassLoader()->getFallbackDirs()) {
110+
$path = $this->getClassLoader()->getFallbackDirs()[0] . '/' . str_replace('\\', '/', $className) . '.php';
111+
112+
return $this->relativizePath($path);
113+
}
114+
115+
return null;
116+
}
117+
118+
public function getNamespacePrefixForClass(string $className): string
119+
{
120+
foreach ($this->getClassLoader()->getPrefixesPsr4() as $prefix => $paths) {
121+
if (0 === strpos($className, $prefix)) {
122+
return $prefix;
123+
}
124+
}
125+
126+
return '';
127+
}
128+
129+
private function getClassLoader(): ClassLoader
130+
{
131+
if (null === self::$classLoader) {
132+
$autoloadPath = $this->absolutizePath('vendor/autoload.php');
133+
80134
if (!file_exists($autoloadPath)) {
81135
throw new \Exception(sprintf('Could not find the autoload file: "%s"', $autoloadPath));
82136
}
83137

84-
/** @var \Composer\Autoload\ClassLoader $loader */
85-
$loader = require $autoloadPath;
86-
$path = null;
87-
foreach ($loader->getPrefixesPsr4() as $prefix => $paths) {
88-
if (0 === strpos($className, $prefix)) {
89-
return $paths[0].'/'.str_replace('\\', '/', str_replace($prefix, '', $className)).'.php';
90-
}
91-
}
138+
self::$classLoader = require $autoloadPath;
139+
}
92140

93-
return null;
141+
return self::$classLoader;
94142
}
95143

96144
private function absolutizePath($path): string

src/Generator.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\MakerBundle;
1313

1414
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
15+
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
1516
use Symfony\Component\Console\Style\SymfonyStyle;
1617

1718
/**
@@ -22,10 +23,12 @@ final class Generator
2223
{
2324
private $fileManager;
2425
private $pendingOperations = [];
26+
private $namespacePrefix;
2527

26-
public function __construct(FileManager $fileManager)
28+
public function __construct(FileManager $fileManager, $namespacePrefix)
2729
{
2830
$this->fileManager = $fileManager;
31+
$this->namespacePrefix = rtrim($namespacePrefix, '\\');
2932
}
3033

3134
/**
@@ -61,6 +64,56 @@ public function generateFile(string $targetPath, string $templateName, array $va
6164
$this->addOperation($targetPath, $templateName, $variables);
6265
}
6366

67+
/**
68+
* Creates a helper object to get data about a class name.
69+
*
70+
* Examples:
71+
*
72+
* // App\Entity\FeaturedProduct
73+
* $gen->createClassNameDetails('FeaturedProduct', 'Entity');
74+
* $gen->createClassNameDetails('featured product', 'Entity');
75+
*
76+
* // App\Controller\FooController
77+
* $gen->createClassNameDetails('foo', 'Controller', 'Controller');
78+
*
79+
* // App\Controller\Admin\FooController
80+
* $gen->createClassNameDetails('Foo\\Admin', 'Controller', 'Controller');
81+
*
82+
* // App\Controller\Security\Voter\CoolController
83+
* $gen->createClassNameDetails('Cool', 'Security\Voter', 'Voter');
84+
*
85+
* // Full class names can also be passed. Imagine the user has an autoload
86+
* // rule where Cool\Stuff lives in a "lib/" directory
87+
* // Cool\Stuff\BalloonController
88+
* $gen->createClassNameDetails('Cool\\Stuff\\Balloon', 'Controller', 'Controller');
89+
*
90+
* @param string $name The short "name" that will be turned into the class name
91+
* @param string $namespacePrefix Recommended namespace where this class should live, but *without* the "App\\" part
92+
* @param string $suffix Optional suffix to guarantee is on the end of the class
93+
* @param string $validationErrorMessage
94+
* @return ClassNameDetails
95+
*/
96+
public function createClassNameDetails(string $name, string $namespacePrefix, string $suffix = '', string $validationErrorMessage = ''): ClassNameDetails
97+
{
98+
$fullNamespacePrefix = $this->namespacePrefix . '\\' . $namespacePrefix;
99+
if ('\\' === $name[0]) {
100+
// class is already "absolute" - leave it alone (but strip opening \)
101+
$className = substr($name, 1);
102+
} else {
103+
$className = rtrim($fullNamespacePrefix, '\\') . '\\' . Str::asClassName($name, $suffix);
104+
}
105+
106+
Validator::validateClassName($className, $validationErrorMessage);
107+
108+
// if this is a custom class, we may be completely different than the namespace prefix
109+
// the best way can do, is find the PSR4 prefix and use that
110+
if (0 !== strpos($className, $fullNamespacePrefix)) {
111+
$fullNamespacePrefix = $this->fileManager->getNamespacePrefixForClass($className);
112+
}
113+
114+
return new ClassNameDetails($className, $fullNamespacePrefix, $suffix);
115+
}
116+
64117
private function addOperation(string $targetPath, string $templateName, array $variables)
65118
{
66119
if ($this->fileManager->fileExists($targetPath)) {
@@ -92,6 +145,9 @@ public function hasPendingOperations(): bool
92145
return !empty($this->pendingOperations);
93146
}
94147

148+
/**
149+
* Actually writes and file changes that are pending.
150+
*/
95151
public function writeChanges()
96152
{
97153
foreach ($this->pendingOperations as $targetPath => $templateData) {

src/Maker/MakeAuthenticator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4444

4545
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4646
{
47-
$classNameDetails = ClassNameDetails::createFromName(
47+
$classNameDetails = $generator->createClassNameDetails(
4848
$input->getArgument('authenticator-class'),
49-
'App\\Security\\'
49+
'Security\\'
5050
);
5151

5252
$generator->generateClass(

src/Maker/MakeCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4646
{
4747
$commandName = trim($input->getArgument('name'));
48-
$commandClassNameDetails = ClassNameDetails::createFromName(
48+
$commandClassNameDetails = $generator->createClassNameDetails(
4949
$commandName,
50-
'App\\Command\\',
50+
'Command\\',
5151
'Command',
5252
sprintf('The "%s" command name is not valid because it would be implemented by "%s" class, which is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores).', $commandName, Str::asClassName($commandName, 'Command'))
5353
);

src/Maker/MakeController.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,19 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
5555

5656
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
5757
{
58-
$controllerClassNameDetails = ClassNameDetails::createFromName(
58+
$controllerClassNameDetails = $generator->createClassNameDetails(
5959
$input->getArgument('controller-class'),
60-
'App\\Controller\\',
60+
'Controller\\',
6161
'Controller'
6262
);
6363

64-
$templateName = Str::asFilePath($controllerClassNameDetails->getOriginalNameWithoutSuffix()) . '/index.html.twig';
64+
$templateName = Str::asFilePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()) . '/index.html.twig';
6565
$controllerPath = $generator->generateClass(
6666
$controllerClassNameDetails->getFullName(),
6767
'controller/Controller.tpl.php',
6868
[
69-
'route_path' => Str::asRoutePath($controllerClassNameDetails->getOriginalNameWithoutSuffix()),
70-
'route_name' => Str::asRouteName($controllerClassNameDetails->getOriginalNameWithoutSuffix()),
69+
'route_path' => Str::asRoutePath($controllerClassNameDetails->getRelativeNameWithoutSuffix()),
70+
'route_name' => Str::asRouteName($controllerClassNameDetails->getRelativeNameWithoutSuffix()),
7171
'twig_installed' => $this->isTwigInstalled(),
7272
'template_name' => $templateName,
7373
]

src/Maker/MakeEntity.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545

4646
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4747
{
48-
$entityClassDetails = ClassNameDetails::createFromName(
48+
$entityClassDetails = $generator->createClassNameDetails(
4949
$input->getArgument('entity-class'),
50-
'App\\Entity\\'
50+
'Entity\\'
5151
);
5252

53-
$repositoryClassDetails = ClassNameDetails::createFromName(
53+
$repositoryClassDetails = $generator->createClassNameDetails(
5454
$entityClassDetails->getOriginalName(),
55-
'App\\Repository\\',
55+
'Repository\\',
5656
'Repository'
5757
);
5858

src/Maker/MakeFixtures.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4646

4747
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4848
{
49-
$fixturesClassNameDetails = ClassNameDetails::createFromName(
49+
$fixturesClassNameDetails = $generator->createClassNameDetails(
5050
$input->getArgument('fixtures-class'),
51-
'App\\DataFixtures\\'
51+
'DataFixtures\\'
5252
);
5353

5454
$generator->generateClass(

src/Maker/MakeForm.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,23 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4646

4747
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4848
{
49-
$formClassNameDetails = ClassNameDetails::createFromName(
49+
$formClassNameDetails = $generator->createClassNameDetails(
5050
$input->getArgument('name'),
51-
'App\\Form\\',
51+
'Form\\',
5252
'Type'
5353
);
5454

55-
$entityClassNameDetails = ClassNameDetails::createFromName(
56-
$formClassNameDetails->getOriginalNameWithoutSuffix(),
57-
'App\\Entity\\'
55+
$entityClassNameDetails = $generator->createClassNameDetails(
56+
$formClassNameDetails->getRelativeNameWithoutSuffix(),
57+
'Entity\\'
5858
);
5959

6060
$generator->generateClass(
6161
$formClassNameDetails->getFullName(),
6262
'form/Type.tpl.php',
6363
[
6464
'entity_full_class_name' => $entityClassNameDetails->getFullName(),
65-
'entity_class_name' => $entityClassNameDetails->getShortName(),
65+
'entity_class_name' => $entityClassNameDetails->getFullName(),
6666
]
6767
);
6868

src/Maker/MakeFunctionalTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4646

4747
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4848
{
49-
$testClassNameDetails = ClassNameDetails::createFromName(
49+
$testClassNameDetails = $generator->createClassNameDetails(
5050
$input->getArgument('name'),
51-
'App\\Tests\\',
51+
'Tests\\',
5252
'Test'
5353
);
5454

src/Maker/MakeSerializerEncoder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545

4646
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4747
{
48-
$encoderClassNameDetails = ClassNameDetails::createFromName(
48+
$encoderClassNameDetails = $generator->createClassNameDetails(
4949
$input->getArgument('name'),
50-
'App\\Serializer\\',
50+
'Serializer\\',
5151
'Encoder'
5252
);
5353
$format = $input->getArgument('format');

src/Maker/MakeSubscriber.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
7171

7272
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
7373
{
74-
$subscriberClassNameDetails = ClassNameDetails::createFromName(
74+
$subscriberClassNameDetails = $generator->createClassNameDetails(
7575
$input->getArgument('name'),
76-
'App\\EventSubscriber\\',
76+
'EventSubscriber\\',
7777
'Subscriber'
7878
);
7979

src/Maker/MakeTwigExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545

4646
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4747
{
48-
$extensionClassNameDetails = ClassNameDetails::createFromName(
48+
$extensionClassNameDetails = $generator->createClassNameDetails(
4949
$input->getArgument('name'),
50-
'App\\Twig\\',
50+
'Twig\\',
5151
'Extension'
5252
);
5353

src/Maker/MakeUnitTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4444

4545
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4646
{
47-
$testClassNameDetails = ClassNameDetails::createFromName(
47+
$testClassNameDetails = $generator->createClassNameDetails(
4848
$input->getArgument('name'),
49-
'App\\Tests\\',
49+
'Tests\\',
5050
'Test'
5151
);
5252

src/Maker/MakeValidator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545

4646
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4747
{
48-
$validatorClassNameDetails = ClassNameDetails::createFromName(
48+
$validatorClassNameDetails = $generator->createClassNameDetails(
4949
$input->getArgument('name'),
50-
'App\\Validator\\',
50+
'Validator\\',
5151
'Validator'
5252
);
5353

src/Maker/MakeVoter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4545

4646
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
4747
{
48-
$voterClassNameDetails = ClassNameDetails::createFromName(
48+
$voterClassNameDetails = $generator->createClassNameDetails(
4949
$input->getArgument('name'),
50-
'App\\Security\\Voter\\',
50+
'Security\\Voter\\',
5151
'Voter'
5252
);
5353

0 commit comments

Comments
 (0)