Junio 2011
faltaba 1 año para...
Symfony 2.0 RC1
Bundle
¿Cuantos bundles deberían haber?
¡Uno para toda la App!
¡Buuuu!
¡Fueraaaaa!
¡Un Bundle por grupo!
Best Practices 2014
1 Bundle - src/AppBundle
- Reducimos complejidad
- Reducimos sistema de carpetas
- Incrementamos rendimiento de trabajo
- Reducimos separación semántica
- Debemos ser más exigentes con el naming
- Complicación si se quiere separar en un futuro
¿Cuantos bundles deberían haber... si hago...?
- MVP
- App + mantenimiento
- Bundle FOS
- Experimento
Nos hemos olvidado lo más importante de todo...
- Como separamos el bundle
- Cual es nuestra lógica de negocio
- Cuales son las best practices
Salir de nuestra zona de confort
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* This bundle will rule the world
*/
class AppBundle extends Bundle
{
}
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* This bundle will rule the world
*/
class AppBundle extends Bundle
{
}
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* This bundle will rule the world
*/
class AppBundle extends Bundle
{
}
- Clase Bundle
- Extensión
- CompilerPass
- Desacoplando
- Clase Bundle
- Extensión
- CompilerPass
- Desacoplando
namespace Symfony\Component\HttpKernel\Bundle;
/**
* An implementation of BundleInterface that adds a few conventions
* for DependencyInjection extensions and Console commands.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class Bundle implements BundleInterface
{
}
definición Extensión
/**
* Returns the bundle's container extension.
*
* @return ExtensionInterface|null The container extension
*
* @throws \LogicException
*/
public function getContainerExtension()
{
if (null === $this->extension) {
$extension = $this->createContainerExtension();
if (null !== $extension) {
if (!$extension instanceof ExtensionInterface) {
throw new \LogicException(sprintf('Extension %s must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', get_class($extension)));
}
// check naming convention
$basename = preg_replace('/Bundle$/', '', $this->getName());
$expectedAlias = Container::underscore($basename);
if ($expectedAlias != $extension->getAlias()) {
throw new \LogicException(sprintf(
'Users will expect the alias of the default extension of a bundle to be the underscored version of the bundle name ("%s"). You can override "Bundle::getContainerExtension()" if you want to use "%s" or another alias.',
$expectedAlias, $extension->getAlias()
));
}
$this->extension = $extension;
} else {
$this->extension = false;
}
}
if ($this->extension) {
return $this->extension;
}
}
- Te busca automáticamente una Extensión
- Dentro de /DependencyInjection
- {App}Extension.php
- Ante cualquier error... ¡ni te enteras!
- ¡Ojo con el alias! Solo acepta si es {app}. Sino... claro, los usuarios no lo entenderán
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* My bundle
*/
final class MyBundle extends Bundle
{
/**
* Returns the bundle's container extension.
*
* @return ExtensionInterface|null The container extension
*
* @throws \LogicException
*/
public function getContainerExtension()
{
return new MyExtension();
}
}
Title Text
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* My bundle
*/
final class MyBundle extends Bundle
{
/**
* Returns the bundle's container extension.
*
* @return ExtensionInterface|null The container extension
*
* @throws \LogicException
*/
public function getContainerExtension()
{
return null;
}
}
DRY
BaseBundle
http://github.com/mmoreram/BaseBundle
- Bundle base donde extender tus propios bundles
- Añade una capa de simplificación para el gran porcentaje de los casos
- Simples métodos con simples responsabilidades
- Minimiza un poco cierta magia oculta
- Por defecto, un bundle no tiene Extension
- En caso contrario, se lo defines
- Con esto ganas algo importante en tus proyectos, y es saber qué se carga, y cuándo
- Te olvidas de este método-que-todo-lo-hace para siempre
namespace AppBundle;
use Mmoreram\BaseBundle\BaseBundle;
/**
* Class MyBundle.
*/
final class MyBundle extends BaseBundle
{
}
Comandos
/**
* Finds and registers Commands.
*
* Override this method if your bundle commands do not follow the conventions:
*
* * Commands are in the 'Command' sub-directory
* * Commands extend Symfony\Component\Console\Command\Command
*
* @param Application $application An Application instance
*/
public function registerCommands(Application $application)
{
if (!is_dir($dir = $this->getPath().'/Command')) {
return;
}
if (!class_exists('Symfony\Component\Finder\Finder')) {
throw new \RuntimeException('You need the symfony/finder component to register bundle commands.');
}
$finder = new Finder();
$finder->files()->name('*Command.php')->in($dir);
$prefix = $this->getNamespace().'\\Command';
foreach ($finder as $file) {
$ns = $prefix;
if ($relativePath = $file->getRelativePath()) {
$ns .= '\\'.str_replace('/', '\\', $relativePath);
}
$class = $ns.'\\'.$file->getBasename('.php');
if ($this->container) {
$alias = 'console.command.'.strtolower(str_replace('\\', '_', $class));
if ($this->container->has($alias)) {
continue;
}
}
$r = new \ReflectionClass($class);
if ($r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command') && !$r->isAbstract() && !$r->getConstructor()->getNumberOfRequiredParameters()) {
$application->add($r->newInstance());
}
}
}
- Te busca en el directorio /Command todos los comandos que tengas
- En caso que exista un servicio con el nombre `console.command.{nombre-construido-al-tun-tun}` entonces no te lo carga
- ¿Y si quieres tener los comandos en otra carpeta? No, compañeros, tampoco podéis.
/**
* Register Commands.
*
* Disabled as commands are registered as services.
*
* @param Application $application An Application instance
*/
public function registerCommands(Application $application)
{
return;
}
- Por defecto, ningún bundle tiene comandos
- Si un bundle necesita registrar comandos sin querer declararlos como servicios, lo hace específicamente
- De este modo, un bundle sabe, exactamente, con precisión, cuáles son los comandos que está exponiendo
- Veréis que esto tiene mucho sentido cuando separamos bundle de componente
/**
* Register Commands.
*
* Disabled as commands are registered as services.
*
* @param Application $application An Application instance
*/
public function registerCommands(Application $application)
{
return [
new OneCommand(),
new AnotherCommand()
]
}
¡OJO basurita!
¿Alguien sabe exactamente qué ocurre cuando... php bin/console?
protected function registerCommands()
{
if ($this->commandsRegistered) {
return;
}
$this->commandsRegistered = true;
$this->kernel->boot();
$container = $this->kernel->getContainer();
foreach ($this->kernel->getBundles() as $bundle) {
if ($bundle instanceof Bundle) {
$bundle->registerCommands($this);
}
}
if ($container->hasParameter('console.command.ids')) {
foreach ($container->getParameter('console.command.ids') as $id) {
$this->add($container->get($id));
}
}
}
- Cada vez que listas o ejecutas cualquier comando por consola... todos y cada uno de ellos se instancian...
- ... ¡incluidos los que se han declarado como servicios!
- Esto implica que si tenemos un comando declarado como un servicio, y un servicio inyectado que, a su vez, tiene el entity manager inyectado...
- ... como no haya base de datos creada, simplemente, no funciona, no establece conexión y lanza Exception
-
Solución 1- Puedes romper la barrera de creación en cadena de servicios utilizando en algún punto un proxy. - Solución 2 - Puedes utilizar algún sistema de CommandBus, dado que solo levantaría el bus en sí, nada más
-
Solución 3- Utilizas el container entero, tan hermoso y tranquilo, y a dormir. - Solución 4 - Tienes huevos y planteas una solución en el repositorio de Symfony (Proxy casi seguro...)
definición CompilerPass
/**
* Builds bundle.
*
* @param ContainerBuilder $container Container
*/
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CompilerPass1());
$container->addCompilerPass(new CompilerPass2());
}
- No sobreescribamos el método build() entero solo para añadir nuestros compilerpass
- BaseBundle tiene un método para ello
/**
* Return a CompilerPass instance array.
*
* @return CompilerPassInterface[]
*/
public function getCompilerPasses()
{
return [
new CompilerPass1(),
new CompilerPass2(),
];
}
Dependencias
Un bundle es un paquete autosuficiente
¿Alguien se lo cree?
¿Quién ha inyectado alguna vez el servicio @router?
- Si instancio mi bundle, cuyos servicios necesitan inyectar @router... Necesito "si o si" instanciar FrameworkBundle
- Al concepto "si o si" se le llama comunmente dependencia
- Volvemos a Symfony 2.0 con su estupendo motor de dependencias... ¡el archivo deps!
- Composer resuelve las dependencias de la mera existencia del paquete, por lo que framework-bundle DEBE estar en composer.yml
- ¿Como solucionamos el hecho que cada bundle exponga sus dependencias a nivel de Kernel?
Adivinad qué pasó...
- Un bundle, ahora mismo, NO es algo autosuficiente, por lo que algún mecanismo debe solucionar este problema
- El repositorio Symfony Bundle Dependencies soluciona tanto la parte de dependencias de bundles, como la resolución por parte del Kernel
- github.com/mmoreram/symfony-bundle-dependencies
- Cualquier bundle que extienda BaseBundle puede definir sus dependencias (por defecto, vacío)
/**
* Create instance of current bundle, and return dependent bundle namespaces.
*
* @return array Bundle instances
*/
public static function getBundleDependencies(KernelInterface $kernel)
{
return [];
}
/**
* Create instance of current bundle, and return dependent bundle namespaces.
*
* @return array Bundle instances
*/
public static function getBundleDependencies(KernelInterface $kernel)
{
return [
'Another\Bundle',
'Even\Another\Bundle',
];
}
- Clase Bundle
- Extensión
- CompilerPass
- Desacoplando
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $config);
/**
* Setting all config elements as DI parameters to inject them
*/
$container->setParameter(
'my.bundle.service_provider',
$config['service_provider']
);
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__ . '/../Resources/config')
);
/**
* Loading DI definitions
*/
$loader->load('classes.yml');
$loader->load('services.yml');
$loader->load('commands.yml');
- 1 método, mucha responsabilidad
- Culpable de la pereza de hacer un bundle
- No tiene buena relación complejidad/impacto
- Muy habitualmente hacemos siempre lo mismo
use Mmoreram\BaseBundle\DependencyInjection\BaseExtension;
/**
* 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.bundle.service_provider' => $config['service_provider'],
];
}
¿Cuantos inyectáis parámetros en vuestros servicios?
¡OJO basurita!
- En caso que compartáis el bundle, nadie os asegura que el valor del parámetro sea bueno
- Si el bundle requiere que el proyecto final defina un parámetro, éste debe validarlo
- Y esto, queridos amigos, se hace en el desconocido archivo Configuration
/**
* 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.bundle.service_provider' => $config['service_provider'],
];
}
- Clase Bundle
- Extensión
- CompilerPass
- Desacoplando
- Un CompilerPass, entre otras cosas permite hacer búsqueda por tags
- Hay un caso bastante habitual, cuando queremos añadir un set de servicios que implementan la misma interface a otra clase del tipo colector
- Por ejemplo una clase que agrupa a todos los shipping providers y itera sobre ellos para determinar cual es el primero que puede actuar
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class MailTransportPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
// always first check if the primary service is defined
if (!$container->has('app.mailer_transport_chain')) {
return;
}
$definition = $container->findDefinition('app.mailer_transport_chain');
// find all service IDs with the app.mail_transport tag
$taggedServices = $container->findTaggedServiceIds('app.mail_transport');
foreach ($taggedServices as $id => $tags) {
// add the transport service to the ChainTransport service
$definition->addMethodCall('addTransport', array(new Reference($id)));
}
}
}
- Siempre, exactamente siempre, lo mismo
- Un método que hace muchas cosas
- Podemos utilizar AbstractTagCompilerPass, pensado para que solo definas los tres valores imprescindibles para dicha acción
use Mmoreram\BaseBundle\CompilerPass\TagCompilerPass;
/**
* Class FeederCompilerPass.
*/
final class FeederCompilerPass extends TagCompilerPass
{
}
/**
* Get collector service name.
*
* @return string Collector service name
*/
public function getCollectorServiceName()
{
return 'my.collector.service';
}
/**
* Get collector method name.
*
* @return string Collector method name
*/
public function getCollectorMethodName()
{
return 'addClass';
}
/**
* Get tag name.
*
* @return string Tag name
*/
public function getTagName()
{
return 'my.tag';
}
- Clase Bundle
- Extensión
- CompilerPass
- Desacoplando
- ¿Que pasa cuando nuestro bundle contiene una capa de servicios que queremos compartir?
- ¿Tenemos claro exactamente qué es un bundle?
Bundle
App Symfony
¿Que pasa cuando alguien depende de nosotros?
Bundle
App Symfony
LIbrería externa PHP
- Una simple librería PHP tendría como dependencia un Bundle y sus dependencias
- No tiene sentido, solo quiere unas clases PHP
- Conceptualmente un Bundle es la forma que tenemos de exponer nuestro código PHP al framework Symfony
- Debemos convertir nuestro esquema de 2 actores a uno de 3
Librería PHP
Bundle
App Symfony
LIbrería externa PHP
Bundle externo
Librería PHP
Bundle
App Symfony
Transformación
-
En un bundle se pone todo lo necesario para que la librería PHP creada se pueda exponer al framework
-
La clase Bundle
-
Los mappings de Doctrine
-
Las clases para el Dependency Injection (Extension, CompilerPass, Configuration)
-
Los archivos yml de configuración
-
En un componente ponemos todo lo que depende de librerías PHP y no es exclusivo de Symfony Framework
-
Esto incluye clases, managers, entidades, factories, controladores, comandos...
-
La clase Bundle, aún dependiendo de una librería PHP (HttpKernel), solo es funcional en el Framework
- Solo es útil cuando
- Se trabaja con proyectos FOS. Se suele crear organizaciones en Github y se publican ambos
- Se traslada la mentalidad FOS a un scope más reducido (una empresa)
- Se espera que haya más integraciones de la librería PHP que la de Symfony (Laravel, Drupal)
- Hay mucho que hacer aún en cuanto a Bundles se refiere
- Cuando uno trabaja con un Framework es importante que conozca algunos de sus puntos críticos
- Os animo a que dediquéis tiempo a comprender qué hay detrás de cierta magia y perdáis el miedo
Conclusiones
- github.com/mmoreram
- twitter.com/mmoreram
- yuhu@mmoreram.com
¡Gracias!
Bundles
By Marc Morera Merino
Bundles
Buenas prácticas para crear Bundles de alta calidad
- 1,857