mmoreram / base-bundle
所有标准 Symfony Bundles 的基础包
Requires
- php: ^8
- mmoreram/symfony-bundle-dependencies: ^2.3
- symfony/framework-bundle: ^5.1
- symfony/yaml: ^5.1
Requires (Dev)
- phpunit/phpunit: ^9
- symfony/browser-kit: ^5.1
- symfony/config: ^5.1
- symfony/console: ^5.1
- symfony/http-kernel: ^5.1
- symfony/process: ^5.1
README
本包的最低要求是 PHP 7.2 和 Symfony 4.3,因为该包使用了这两个版本的特性。如果您还没有使用它们,我鼓励您这样做。
关于内容
本包旨在成为您 Symfony 项目中所有包的基础。了解这些大块。
包
功能测试
包扩展
Symfony 中的所有包都应该从一个 PHP 类开始,即 Bundle 类。这个类应该始终实现接口 Symfony\Component\HttpKernel\Bundle\BundleInterface
,但如您所知,Symfony 总是试图让事情变得简单,所以您可以直接扩展包的基本实现。
use Symfony\Component\HttpKernel\Bundle\Bundle; /** * My bundle */ final class MyBundle extends Bundle { }
我个人捍卫了框架某些部分的魔法,但您应该始终了解那个魔法以及它如何影响您的项目。让我通过这个实现简单地解释一下您的包行为。
包依赖
当我们谈到依赖关系时,我们习惯于谈论 PHP 依赖关系。如果我们使用一个文件,那么这个文件应该在我们的 vendor 文件夹内,对吧?这听起来很棒,但如果一个包需要在我们的 kernel 中实例化另一个包呢?Symfony 如何处理这种需求呢?
目前项目本身并没有提供这个功能,但即使理论说一个包不应该有外部包依赖,现实却是另一回事,而且据我所知,实现主要解决现实问题而不是美好的理论。
让我们看看 Symfony Bundle Dependencies。通过使用这个 BaseBundle,您的包将自动具有依赖关系(默认为无)。
use Symfony\Component\HttpKernel\Bundle\Bundle; use Mmoreram\SymfonyBundleDependencies\DependentBundleInterface; /** * Class AbstractBundle. */ abstract class BaseBundle extends Bundle implements DependentBundleInterface { //... /** * Create instance of current bundle, and return dependent bundle namespaces * * @return array Bundle instances */ public static function getBundleDependencies(KernelInterface $kernel) { return []; } }
如果您的包有依赖关系,请随意在您的类中覆盖此方法并添加它们。查看主库文档以了解更多关于如何在 Kernel 中处理依赖关系的信息。
扩展声明
首先,您的扩展将通过魔法加载。这意味着什么?好吧,框架将按照一个标准(Symfony 的标准)寻找您的扩展。但如果您的扩展(由于错误或明确指定)没有遵循这个标准会怎样呢?
什么也不会发生。框架仍然会寻找一个不存在的类,而您想要的类永远不会被实例化。您将会花费一些宝贵的时间来找出问题所在。
在您的项目中要做的第一步是:避免这种魔法,并始终在您的包中实例化您的扩展来定义它。
use Mmoreram\BaseBundle\BaseBundle; /** * My bundle */ final class MyBundle extends BaseBundle { /** * Returns the bundle's container extension. * * @return ExtensionInterface|null The container extension * * @throws \LogicException */ public function getContainerExtension() { return new MyExtension($this); } }
如您所见,您的扩展将需要包本身作为第一个也是唯一的构造参数。请参阅配置章节以了解原因。
即使这是默认行为,您也可以更明确地覆盖此方法来定义您的包不使用任何扩展。这将有助于您更好地理解您的包需求。
use Mmoreram\BaseBundle\BaseBundle; /** * My bundle */ final class MyBundle extends BaseBundle { /** * Returns the bundle's container extension. * * @return ExtensionInterface|null The container extension * * @throws \LogicException */ public function getContainerExtension() { return null; } }
编译器传递声明
Symfony 最不为人知的特性之一是编译器通过。如果你想了解它们是什么以及如何使用,请查阅出色的食谱 如何在组件中使用编译器通过。
你可以在组件内部使用 build 方法来实例化编译器通过,如本示例所示。
use Symfony\Component\HttpKernel\Bundle\Bundle; /** * My bundle */ final class MyBundle extends Bundle { /** * Builds bundle. * * @param ContainerBuilder $container Container */ public function build(ContainerBuilder $container) { parent::build($container); /** * Adds Compiler Passes. */ $container->addCompilerPass(new MyCompilerPass()); } }
让我们简化一下。使用 BaseBundle,你将能够使用 getCompilerPasses 方法来定义所有编译器通过。
use Mmoreram\BaseBundle\BaseBundle; /** * My bundle */ final class MyBundle extends BaseBundle { /** * Register compiler passes * * @return CompilerPassInterface[] */ public function getCompilerPasses() { return [ new MyCompilerPass(), ]; } }
命令声明
组件还负责将所有命令暴露到主应用程序中。这里也有魔法,所以所有在主文件夹 Command 中以 Command 结尾,并扩展 Command 或 ContainerAwareCommand 的文件都将被实例化和加载,每次组件被实例化时。
与扩展类似。你负责知道你的类的位置,而组件应该非常明确地知道。
默认情况下,这个 BaseBundle 抽象类移除了 Command 自动加载,允许你在主 Bundle 类中返回一个 Command 实例数组。默认情况下,此方法返回空数组。
/** * Class AbstractBundle. */ abstract class BaseBundle extends Bundle { // ... /** * Get command instance array * * @return Command[] */ public function getCommands() : array { return []; } // ... }
我强烈建议你永远不要使用这种魔法来使用命令,因为命令应该像控制器和事件监听器一样,只是你领域的一个入口点。你可以定义命令作为服务,并在其中注入所有使其工作的东西。
SimpleBaseBundle
更简单。
Symfony 应该提供一个 RAD 基础设施,这样,如果你想要创建一个快速组件,将其基本部分暴露给框架,就不会让你花费太多的时间和精力。
因此,对于你的 RAD 应用程序,你真的认为你需要多个类来创建一个简单的组件吗?一点都不要。不再是。
请欢迎 SimpleBaseBundle,它是为 RAD 应用程序创建组件的简单方式。
use Mmoreram\BaseBundle\Mapping\MappingBagProvider; use Mmoreram\BaseBundle\SimpleBaseBundle; use Symfony\Component\HttpKernel\KernelInterface; /** * Class TestSimpleBundle */ class TestSimpleBundle extends SimpleBaseBundle { /** * get config files */ public function getConfigFiles() : array { return [ 'services' ]; } /** * Get command instance array * * @return Command[] */ public function getCommands() : array { return []; } /** * Return a CompilerPass instance array. * * @return CompilerPassInterface[] */ public function getCompilerPasses() { return []; } /** * Create instance of current bundle, and return dependent bundle namespaces. * * @return array Bundle instances */ public static function getBundleDependencies(KernelInterface $kernel) { return []; } }
这就完成了。
使用这个类,你将创建包含其依赖关系的组件,初始化必要的命令和编译器通过(如果需要),加载 yaml 配置文件,并使用在 MappingBagProvider 中定义的配置初始化实体。
不需要创建 DependencyInjection 文件夹。
如果你的项目扩展到另一个维度或质量级别,那么请自由地更改你的组件实现,并开始扩展 BaseBundle 而不是 SimpleBaseBundle。然后,创建所需的 DependencyInjection 文件夹。
扩展
每次你需要创建一个新的组件时都会遇到的一个痛点。组件扩展是组件本身与所有依赖注入环境之间的某种端口。你可能习惯于看到这样的文件。
use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** * This is the class that loads and manages your bundle configuration */ class MyExtension extends Extension { /** * Loads a specific configuration. * * @param array $config An array of configuration values * @param ContainerBuilder $container A ContainerBuilder instance * * @throws \InvalidArgumentException When provided tag is not defined in this extension * * @api */ public function load(array $config, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $config); /** * Setting all config elements as DI parameters to inject them */ $container->setParameter( 'my_parameter', $config['my_parameter'] ); $loader = new YamlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); /** * Loading DI definitions */ $loader->load('services.yml'); $loader->load('commands.yml'); $loader->load('controllers.yml'); } }
扩展 BaseExtension
很难记住,对吗?好吧,这应该不再是问题。看看使用 BaseExtension 的这个实现。
use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** * This is the class that loads and manages your bundle configuration */ class MyExtension extends BaseExtension { /** * Returns the recommended alias to use in XML. * * This alias is also the mandatory prefix to use when using YAML. * * @return string The alias * * @api */ public function getAlias() { return 'app'; } /** * Return a new Configuration instance. * * If object returned by this method is an instance of * ConfigurationInterface, extension will use the Configuration to read all * bundle config definitions. * * Also will call getParametrizationValues method to load some config values * to internal parameters. * * @return ConfigurationInterface Configuration file */ protected function getConfigurationInstance() { return new Configuration(); } /** * Get the Config file location. * * @return string Config file location */ protected function getConfigFilesLocation() { return __DIR__ . '/../Resources/config'; } /** * Config files to load. * * Each array position can be a simple file name if must be loaded always, * or an array, with the filename in the first position, and a boolean in * the second one. * * As a parameter, this method receives all loaded configuration, to allow * setting this boolean value from a configuration value. * * return array( * 'file1', * 'file2', * ['file3', $config['my_boolean'], * ... * ); * * @param array $config Config definitions * * @return array Config files */ protected function getConfigFiles(array $config) { return [ 'services', 'commands', 'controllers', ]; } /** * Load Parametrization definition. * * return array( * 'parameter1' => $config['parameter1'], * 'parameter2' => $config['parameter2'], * ... * ); * * @param array $config Bundles config values * * @return array Parametrization values */ protected function getParametrizationValues(array $config) { return [ 'my_parameter' => $config['my_parameter'], ]; } }
也许文件更大,你可能注意到代码行数更多,但它似乎更容易理解,对吧?这就是整洁代码的含义。这个类只会做一件事。你的服务定义使用 yml 格式。这是因为它比 XML 和 PHP 更清晰,因为它更容易被人类解释。正如你在 getConfigFiles 方法中看到的,你返回不带扩展名的文件名,这总是 yml。
你还可以使用这两个方法在容器加载前后修改容器。
//... /** * Hook after pre-pending configuration. * * @param array $config Configuration * @param ContainerBuilder $container Container */ protected function preLoad(array $config, ContainerBuilder $container) { // Implement here your bundle logic } /** * Hook after load the full container. * * @param array $config Configuration * @param ContainerBuilder $container Container */ protected function postLoad(array $config, ContainerBuilder $container) { // Implement here your bundle logic } //...
配置
你的组件将如何从外部(app)请求和验证一些数据的方式是通过一个配置文件。如果你想了解一点关于这个令人惊叹的功能的信息,可以查看官方的 配置文档。
让我们为我们的包创建一个新的配置文件,并让我们发现这个库通过扩展配置文件将为您提供的某些不错特性。
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Mmoreram\BaseBundle\DependencyInjection\BaseConfiguration; /** * Class AppConfiguration. */ class AppConfiguration extends BaseConfiguration { /** * {@inheritdoc} */ protected function setupTree(ArrayNodeDefinition $rootNode) { $rootNode ->children() ->arrayNode('skills') ->prototype('scalar') ->end() ->end() ->end(); } }
哎呀!这里发生了什么?让我们一步一步地检查一下。
扩展别名
首先,配置文件将不再定义它自己的名称。配置文件应该只定义应该请求到应用的数据类型,而不是在什么命名空间下。
那么,谁应该定义这个命名空间呢?扩展应该作为真正扩展依赖注入环境的那个。换句话说,即使这绝对不是你的场景,你也应该能够在不同的扩展之间共享一个配置文件。
那么……我们如何做到这一点呢?如果你的配置文件扩展了这个文件,那么,只要你想初始化它,你将不得不定义它的命名空间。看看这个例子。这个方法是你的扩展文件的一部分。
/** * Return a new Configuration instance. * * If object returned by this method is an instance of * ConfigurationInterface, extension will use the Configuration to read all * bundle config definitions. * * Also will call getParametrizationValues method to load some config values * to internal parameters. * * @return ConfigurationInterface Configuration file */ protected function getConfigurationInstance() { return new Configuration( $this->getAlias() ); }
扩展 BaseConfiguration
通过扩展BaseConfiguration类,你将在构造函数中默认获得这个别名参数。
/** * Return a new Configuration instance. * * If object returned by this method is an instance of * ConfigurationInterface, extension will use the Configuration to read all * bundle config definitions. * * Also will call getParametrizationValues method to load some config values * to internal parameters. * * @return ConfigurationInterface Configuration file */ protected function getConfigurationInstance() { return new BaseConfiguration( $this->getAlias() ); }
使用这个类,你再也不用担心如何创建验证树了。只需在你的扩展定义的别名下定义验证树即可。
/** * Configure the root node. * * @param ArrayNodeDefinition $rootNode Root node */ protected function setupTree(ArrayNodeDefinition $rootNode) { $rootNode ->children() ... }
默认情况下,如果你没有重写这个方法,你的包下将不会添加任何参数化。
功能测试
当许多项目想要以功能方式开始测试他们的包时,他们真正不知道如何处理内核。要遵循的步骤始终是相同的。
- 创建一个小包来测试你的特性
- 创建一个作为独立应用运行的内核
- 为该内核创建配置
但是,如果我们想针对多个内核和不同的内核配置进行测试,就会出现一些问题。
我们该如何解决这个问题呢?
好吧,这不再是一个问题,至少使用这个库是这样的。让我们看看一个功能测试和从现在开始你可以如何进行测试。
use Mmoreram\BaseBundle\Kernel\BaseKernel; use Mmoreram\BaseBundle\Tests\BaseFunctionalTest; /** * Class MyTest. */ final class MyTest extends BaseFunctionalTest { /** * Get kernel. * * @return KernelInterface */ protected static function getKernel() : KernelInterface { return new BaseKernel( [ 'Mmoreram\BaseBundle\Tests\Bundle\TestBundle', ], [ 'services' => [ 'my.service' => [ 'class' => 'My\Class', 'arguments' => [ "a string", "@another.service" ] ] ], 'parameters' => [ 'locale' => 'es' ], 'framework' => [ 'form' => true ] ], [ ['/login', '@MyBundle:User:login', 'user_login'], ['/logout', '@MyBundle:User:logout', 'user_logout'], ] ); } /** * Test compiler pass. */ public function testCompilerPass() { // do your tests } }
正如你所看到的,你可以做很多事情,以便创建一个独特的场景。通过一个简单的类(你的测试),你可以定义你所有应用程序的环境。
让我们一步一步地看看你可以在这里做什么
BaseKernel
这个库为你提供了一个特殊的内核,用于你的测试。这个内核是测试就绪的,并允许你在每个场景中根据需要自定义应用程序。每个测试类都将使用一个独特的内核配置,因此这个测试类中的所有测试用例都将针对这个内核执行。
这个内核默认使用Symfony Bundle Dependencies项目,所以请确保你查看这个项目。使用它不是必须的,但是一个很好的选择。
让我们看看你需要使用这个库提供的工具来创建自己的内核。
new BaseKernel( [ 'Mmoreram\BaseBundle\Tests\Bundle\TestBundle', ], [ 'imports' => [ ['resource' => '@BaseBundle/Resources/config/providers.yml'], ], 'services' => [ 'my.service' => [ 'class' => 'My\Class', 'arguments' => [ "a string", "@another.service" ] ] ], 'parameters' => [ 'locale' => 'es' ], 'framework' => [ 'form' => true ] ], [ ['/login', '@MyBundle:User:login', 'user_login'], ['/logout', '@MyBundle:User:logout', 'user_logout'], '@MyBundle/Resources/routing.yml', ] );
内核创建只需要三个参数。
-
你需要实例化内核的包命名空间数组。如果你不想使用Symfony Bundle Dependencies项目,确保你添加所有这些。否则,如果你使用该项目,你应该只添加你想要测试的包。
-
依赖注入组件的配置。使用与之前使用yml文件相同的格式,但在PHP中。
-
路由。你可以定义由三个位置组成的单个路由数组。第一个是路径,第二个是控制器表示法,第三个是路由名称。你可以使用资源名称来定义资源。
在您的配置定义中,由于大多数测试用例都可以针对 FrameworkBundle 和/或 DoctrineBundle 执行,您可以通过在配置数组中添加以下行来为每个包预加载一个简单的配置。
new BaseKernel( [ 'Mmoreram\BaseBundle\Tests\Bundle\TestBundle', ], [ 'imports' => [ ['resource' => '@BaseBundle/Resources/config/providers.yml'], ['resource' => '@BaseBundle/Resources/test/framework.test.yml'], ['resource' => '@BaseBundle/Resources/test/doctrine.test.yml'], ], 'services' => [ 'my.service' => [ 'class' => 'My\Class', 'arguments' => [ "a string", "@another.service" ] ] ], ], [ ['/login', '@MyBundle:User:login', 'user_login'], ['/logout', '@MyBundle:User:logout', 'user_logout'], '@MyBundle/Resources/routing.yml', ] );
缓存和日志
这里的问题是...好吧,但我在哪里可以找到我的 Kernel 缓存和日志呢?嗯,每个内核配置(包、配置和路由)都会被哈希成一个唯一的字符串。然后,系统会在 var/test
文件夹下创建一个文件夹,并在其中创建一个唯一的 {hash}
文件夹。
每次您重复使用相同的内核配置时,这个先前生成的缓存将被用来提高测试的性能。
为了进一步提高性能,请毫不犹豫地在这个 var/test/
文件夹内创建一个 tmpfs,使用以下命令。
sudo mount -t tmpfs -o size=512M tmpfs var/test/
BaseFunctionalTest
一旦您定义了如何实例化您的内核,我们应该创建我们的第一个功能测试。让我们看看我们如何做到这一点。
use Mmoreram\BaseBundle\Tests\BaseFunctionalTest; use Mmoreram\BaseBundle\Tests\BaseKernel; /** * Class TagCompilerPassTest. */ final class TagCompilerPassTest extends BaseFunctionalTest { /** * Get kernel. * * @return KernelInterface */ protected static function getKernel() : KernelInterface { return $kernel; } /** * Test compiler pass. */ public function testCompilerPass() { // do your tests } }
在每个场景中,您的内核都会被创建并本地保存。您可以创建自己的内核或使用 BaseKernel,在两种情况下这都将正常工作,但请注意,这个内核将在整个场景中保持活动状态。
快速测试方法
功能测试应该仅测试应用行为,因此我们应该能够减少所有与此不相关的操作。
BaseFunctionalTest 拥有一套易于使用的方法。
->get()
如果您想使用任何容器服务,只需调用此方法(如控制器中所示)
$this->assetInstanceOf( '\MyBundle\My\Service\Namespace', $this->get('service_name') );
->has()
如果您想检查容器服务是否存在,请调用此方法。对于服务存在测试很有用
$this->assertTrue( $this->has('service_name') );
->getParameter()
如果您想使用任何容器参数,请调用此方法(如控制器中所示)
$this->assertEqual( 'en', $this->getParameter('locale') );