[HttpKernel][FrameworkBundle] Add alternative convention for bundle directories

This commit is contained in:
Yonel Ceruto 2019-07-31 22:33:32 -04:00 committed by Nicolas Grekas
parent 6723bb9cdf
commit 6996e1cbe2
29 changed files with 308 additions and 42 deletions

View File

@ -108,9 +108,33 @@ HttpFoundation
HttpKernel
----------
* Implementing the `BundleInterface` without implementing the `getPublicDir()` method is deprecated.
This method will be added to the interface in 5.0.
* The `DebugHandlersListener` class has been marked as `final`
* Added new Bundle directory convention consistent with standard skeletons:
```
└── MyBundle/
├── config/
├── public/
├── src/
│ └── MyBundle.php
├── templates/
└── translations/
```
To make this work properly, it is necessary to change the root path of the bundle:
```php
class MyBundle extends Bundle
{
public function getPath(): string
{
return \dirname(__DIR__);
}
}
```
As many bundles must be compatible with a range of Symfony versions, the current
directory convention is not deprecated yet, but it will be in the future.
Lock
----

View File

@ -286,7 +286,6 @@ HttpFoundation
HttpKernel
----------
* The `getPublicDir()` method has been added to the `BundleInterface`.
* Removed `Client`, use `HttpKernelBrowser` instead
* The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been removed
* The `KernelInterface::getName()` and the `kernel.name` parameter have been removed
@ -303,6 +302,32 @@ HttpKernel
* Removed `TranslatorListener` in favor of `LocaleAwareListener`
* The `DebugHandlersListener` class has been made `final`
* Removed `SaveSessionListener` in favor of `AbstractSessionListener`
* Added new Bundle directory convention consistent with standard skeletons:
```
└── MyBundle/
├── config/
├── public/
├── src/
│ └── MyBundle.php
├── templates/
└── translations/
```
To make this work properly, it is necessary to change the root path of the bundle:
```php
class MyBundle extends Bundle
{
public function getPath(): string
{
return \dirname(__DIR__);
}
}
```
As many bundles must be compatible with a range of Symfony versions, the current
directory convention is not deprecated yet, but it will be in the future.
Intl
----

View File

@ -48,11 +48,10 @@ class DoctrineValidationPass implements CompilerPassInterface
}
$files = $container->getParameter('validator.mapping.loader.'.$mapping.'_files_loader.mapping_files');
$validationPath = 'Resources/config/validation.'.$this->managerType.'.'.$extension;
$validationPath = '/config/validation.'.$this->managerType.'.'.$extension;
foreach ($container->getParameter('kernel.bundles') as $bundle) {
$reflection = new \ReflectionClass($bundle);
if ($container->fileExists($file = \dirname($reflection->getFileName()).'/'.$validationPath)) {
foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
if ($container->fileExists($file = $bundle['path'].'/Resources'.$validationPath) || $container->fileExists($file = $bundle['path'].$validationPath)) {
$files[] = $file;
}
}

View File

@ -137,13 +137,7 @@ EOT
$validAssetDirs = [];
/** @var BundleInterface $bundle */
foreach ($kernel->getBundles() as $bundle) {
if (!method_exists($bundle, 'getPublicDir')) {
@trigger_error(sprintf('Not defining "getPublicDir()" method in the "%s" class is deprecated since Symfony 4.4 and will not be supported in 5.0.', \get_class($bundle)), E_USER_DEPRECATED);
$publicDir = 'Resources/public';
} else {
$publicDir = ltrim($bundle->getPublicDir(), '\\/');
}
if (!is_dir($originDir = $bundle->getPath().\DIRECTORY_SEPARATOR.$publicDir)) {
if (!is_dir($originDir = $bundle->getPath().'/Resources/public') && !is_dir($originDir = $bundle->getPath().'/public')) {
continue;
}

View File

@ -162,7 +162,9 @@ EOF
if (null !== $input->getArgument('bundle')) {
try {
$bundle = $kernel->getBundle($input->getArgument('bundle'));
$transPaths = [$bundle->getPath().'/Resources/translations'];
$bundleDir = $bundle->getPath();
$transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations'];
$viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates'];
if ($this->defaultTransPath) {
$transPaths[] = $this->defaultTransPath;
}
@ -171,7 +173,6 @@ EOF
$notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $dir, $bundle->getName());
@trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED);
}
$viewsPaths = [$bundle->getPath().'/Resources/views'];
if ($this->defaultViewsPath) {
$viewsPaths[] = $this->defaultViewsPath;
}
@ -206,13 +207,14 @@ EOF
}
} elseif ($input->getOption('all')) {
foreach ($kernel->getBundles() as $bundle) {
$transPaths[] = $bundle->getPath().'/Resources/translations';
$bundleDir = $bundle->getPath();
$transPaths[] = is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundle->getPath().'/translations';
$viewsPaths[] = is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundle->getPath().'/templates';
if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/translations', $rootDir, $bundle->getName()))) {
$transPaths[] = $deprecatedPath;
$notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath);
@trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED);
}
$viewsPaths[] = $bundle->getPath().'/Resources/views';
if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/views', $rootDir, $bundle->getName()))) {
$viewsPaths[] = $deprecatedPath;
$notice = sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath);

View File

@ -154,7 +154,9 @@ EOF
if (null !== $input->getArgument('bundle')) {
try {
$foundBundle = $kernel->getBundle($input->getArgument('bundle'));
$transPaths = [$foundBundle->getPath().'/Resources/translations'];
$bundleDir = $foundBundle->getPath();
$transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations'];
$viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates'];
if ($this->defaultTransPath) {
$transPaths[] = $this->defaultTransPath;
}
@ -163,7 +165,6 @@ EOF
$notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $foundBundle->getName(), $dir);
@trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED);
}
$viewsPaths = [$foundBundle->getPath().'/Resources/views'];
if ($this->defaultViewsPath) {
$viewsPaths[] = $this->defaultViewsPath;
}

View File

@ -1152,7 +1152,7 @@ class FrameworkExtension extends Extension
$defaultDir = $container->getParameterBag()->resolveValue($config['default_path']);
$rootDir = $container->getParameter('kernel.root_dir');
foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) {
if ($container->fileExists($dir = $bundle['path'].'/Resources/translations')) {
if ($container->fileExists($dir = $bundle['path'].'/Resources/translations') || $container->fileExists($dir = $bundle['path'].'/translations')) {
$dirs[] = $dir;
} else {
$nonExistingDirs[] = $dir;
@ -1314,20 +1314,20 @@ class FrameworkExtension extends Extension
}
foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
$dirname = $bundle['path'];
$configDir = is_dir($bundle['path'].'/Resources/config') ? $bundle['path'].'/Resources/config' : $bundle['path'].'/config';
if (
$container->fileExists($file = $dirname.'/Resources/config/validation.yaml', false) ||
$container->fileExists($file = $dirname.'/Resources/config/validation.yml', false)
$container->fileExists($file = $configDir.'/validation.yaml', false) ||
$container->fileExists($file = $configDir.'/validation.yml', false)
) {
$fileRecorder('yml', $file);
}
if ($container->fileExists($file = $dirname.'/Resources/config/validation.xml', false)) {
if ($container->fileExists($file = $configDir.'/validation.xml', false)) {
$fileRecorder('xml', $file);
}
if ($container->fileExists($dir = $dirname.'/Resources/config/validation', '/^$/')) {
if ($container->fileExists($dir = $configDir.'/validation', '/^$/')) {
$this->registerMappingFilesFromDir($dir, $fileRecorder);
}
}
@ -1508,20 +1508,20 @@ class FrameworkExtension extends Extension
};
foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
$dirname = $bundle['path'];
$configDir = is_dir($bundle['path'].'/Resources/config') ? $bundle['path'].'/Resources/config' : $bundle['path'].'/config';
if ($container->fileExists($file = $dirname.'/Resources/config/serialization.xml', false)) {
if ($container->fileExists($file = $configDir.'/serialization.xml', false)) {
$fileRecorder('xml', $file);
}
if (
$container->fileExists($file = $dirname.'/Resources/config/serialization.yaml', false) ||
$container->fileExists($file = $dirname.'/Resources/config/serialization.yml', false)
$container->fileExists($file = $configDir.'/serialization.yaml', false) ||
$container->fileExists($file = $configDir.'/serialization.yml', false)
) {
$fileRecorder('yml', $file);
}
if ($container->fileExists($dir = $dirname.'/Resources/config/serialization', '/^$/')) {
if ($container->fileExists($dir = $configDir.'/serialization', '/^$/')) {
$this->registerMappingFilesFromDir($dir, $fileRecorder);
}
}

View File

@ -0,0 +1,24 @@
<?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 Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity;
class LegacyPerson
{
public $name;
public $age;
public function __construct(string $name, string $age)
{
$this->name = $name;
$this->age = $age;
}
}

View File

@ -0,0 +1,18 @@
<?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 Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class LegacyBundle extends Bundle
{
}

View File

@ -0,0 +1,4 @@
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson:
attributes:
name:
serialized_name: 'full_name'

View File

@ -0,0 +1,5 @@
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson:
properties:
age:
- GreaterThan:
value: 18

View File

@ -0,0 +1,4 @@
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson:
attributes:
name:
serialized_name: 'full_name'

View File

@ -0,0 +1,5 @@
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson:
properties:
age:
- GreaterThan:
value: 18

View File

@ -0,0 +1,24 @@
<?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 Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity;
class ModernPerson
{
public $name;
public $age;
public function __construct(string $name, string $age)
{
$this->name = $name;
$this->age = $age;
}
}

View File

@ -0,0 +1,22 @@
<?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 Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ModernBundle extends Bundle
{
public function getPath(): string
{
return \dirname(__DIR__);
}
}

View File

@ -0,0 +1,84 @@
<?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 Symfony\Bundle\FrameworkBundle\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Filesystem;
class BundlePathsTest extends AbstractWebTestCase
{
public function testBundlePublicDir()
{
$kernel = static::bootKernel(['test_case' => 'BundlePaths']);
$projectDir = sys_get_temp_dir().'/'.uniqid('sf_bundle_paths', true);
$fs = new Filesystem();
$fs->mkdir($projectDir.'/public');
$command = (new Application($kernel))->add(new AssetsInstallCommand($fs, $projectDir));
$exitCode = (new CommandTester($command))->execute(['target' => $projectDir.'/public']);
$this->assertSame(0, $exitCode);
$this->assertFileExists($projectDir.'/public/bundles/modern/modern.css');
$this->assertFileExists($projectDir.'/public/bundles/legacy/legacy.css');
$fs->remove($projectDir);
}
public function testBundleTwigTemplatesDir()
{
static::bootKernel(['test_case' => 'BundlePaths']);
$twig = static::$container->get('twig');
$bundlesMetadata = static::$container->getParameter('kernel.bundles_metadata');
$this->assertSame([$bundlesMetadata['LegacyBundle']['path'].'/Resources/views'], $twig->getLoader()->getPaths('Legacy'));
$this->assertSame("OK\n", $twig->render('@Legacy/index.html.twig'));
$this->assertSame([$bundlesMetadata['ModernBundle']['path'].'/templates'], $twig->getLoader()->getPaths('Modern'));
$this->assertSame("OK\n", $twig->render('@Modern/index.html.twig'));
}
public function testBundleTranslationsDir()
{
static::bootKernel(['test_case' => 'BundlePaths']);
$translator = static::$container->get('translator');
$this->assertSame('OK', $translator->trans('ok_label', [], 'legacy'));
$this->assertSame('OK', $translator->trans('ok_label', [], 'modern'));
}
public function testBundleValidationConfigDir()
{
static::bootKernel(['test_case' => 'BundlePaths']);
$validator = static::$container->get('validator');
$this->assertTrue($validator->hasMetadataFor(LegacyPerson::class));
$this->assertCount(1, $constraintViolationList = $validator->validate(new LegacyPerson('john', 5)));
$this->assertSame('This value should be greater than 18.', $constraintViolationList->get(0)->getMessage());
$this->assertTrue($validator->hasMetadataFor(ModernPerson::class));
$this->assertCount(1, $constraintViolationList = $validator->validate(new ModernPerson('john', 5)));
$this->assertSame('This value should be greater than 18.', $constraintViolationList->get(0)->getMessage());
}
public function testBundleSerializationConfigDir()
{
static::bootKernel(['test_case' => 'BundlePaths']);
$serializer = static::$container->get('serializer');
$this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new LegacyPerson('john', 5), 'json'));
$this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new ModernPerson('john', 5), 'json'));
}
}

View File

@ -0,0 +1,22 @@
<?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.
*/
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\LegacyBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\ModernBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
return [
new FrameworkBundle(),
new TwigBundle(),
new ModernBundle(),
new LegacyBundle(),
];

View File

@ -0,0 +1,11 @@
imports:
- { resource: ../config/default.yml }
framework:
translator: true
validation: true
serializer: true
twig:
strict_variables: '%kernel.debug%'
exception_controller: ~

View File

@ -188,7 +188,7 @@ class TwigExtension extends Extension
}
$container->addResource(new FileExistenceResource($defaultOverrideBundlePath));
if (file_exists($dir = $bundle['path'].'/Resources/views')) {
if (file_exists($dir = $bundle['path'].'/Resources/views') || file_exists($dir = $bundle['path'].'/templates')) {
$bundleHierarchy[$name][] = $dir;
}
$container->addResource(new FileExistenceResource($dir));

View File

@ -65,9 +65,11 @@ class TemplateIterator implements \IteratorAggregate
$name = substr($name, 0, -6);
}
$bundleTemplatesDir = is_dir($bundle->getPath().'/Resources/views') ? $bundle->getPath().'/Resources/views' : $bundle->getPath().'/templates';
$templates = array_merge(
$templates,
$this->findTemplatesInDirectory($bundle->getPath().'/Resources/views', $name),
$this->findTemplatesInDirectory($bundleTemplatesDir, $name),
$this->findTemplatesInDirectory($this->rootDir.'/Resources/'.$bundle->getName().'/views', $name)
);
if (null !== $this->defaultPath) {

View File

@ -135,11 +135,6 @@ abstract class Bundle implements BundleInterface
{
}
public function getPublicDir(): string
{
return 'Resources/public';
}
/**
* Returns the bundle's container extension class.
*

View File

@ -19,8 +19,6 @@ use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
* BundleInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @method string getPublicDir() Returns relative path for the public assets directory
*/
interface BundleInterface extends ContainerAwareInterface
{

View File

@ -4,9 +4,8 @@ CHANGELOG
4.4.0
-----
* Implementing the `BundleInterface` without implementing the `getPublicDir()` method is deprecated.
This method will be added to the interface in 5.0.
* The `DebugHandlersListener` class has been marked as `final`
* Added new Bundle directory convention consistent with standard skeletons
4.3.0
-----