Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Constructor en Symfony y con una Fábrica

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

¿Y si, para instanciar los objetos Character, CharacterBuilder necesitara, por ejemplo, hacer una consulta a la base de datos? Bien, cuando necesitamos hacer una consulta, normalmente damos a nuestra clase un constructor y luego autocontratamos el servicio del gestor de entidades. Pero CharacterBuilder no es un servicio. Técnicamente podrías utilizarlo como un servicio, pero un servicio es una clase de la que normalmente sólo necesitas una única instancia en tu aplicación. Sin embargo, en GameApplication estamos creando unCharacterBuilder por personaje. Si intentáramos autoconducir CharacterBuildera GameApplication, eso funcionaría. Symfony autocablearía el EntityManager en CharacterBuilder y luego autocablearía ese objeto CharacterBuilder aquí. El problema es que entonces sólo tendríamos un CharacterBuilder... cuando en realidad necesitamos cuatro para crear nuestros cuatro objetos Character.

Por eso los objetos constructores suelen ir asociados a una fábrica de constructores. Déjame deshacer todos los cambios que acabo de hacer en GameApplication... y en CharacterBuilder.

Crear una fábrica

En el directorio Builder/, crea una nueva clase llamada CharacterBuilderFactory:

... lines 1 - 2
namespace App\Builder;
class CharacterBuilderFactory
{
... lines 7 - 10
}

Por cierto, existe un patrón llamado patrón de fábrica, que no trataremos específicamente en este tutorial. Pero una "fábrica" no es más que una clase cuyo trabajo es crear otra clase. Al igual que el patrón constructor, es un patrón de creación. Dentro de la clase fábrica, crea un nuevo método llamado, qué tal...createBuilder(), que devolverá un CharacterBuilder. Y dentro de éste, simplemente return new CharacterBuilder():

... lines 1 - 4
class CharacterBuilderFactory
{
public function createBuilder(): CharacterBuilder
{
return new CharacterBuilder();
}
}

Este CharacterBuilderFactory es un servicio. Aunque necesitemos cinco objetosCharacterBuilder en nuestra aplicación, sólo necesitaremos un CharacterBuilderFactory. Simplemente llamaremos a este método cinco veces.

Eso significa que, en GameApplication, podemos crear un public function __construct()y autoconectar CharacterBuilderFactory $characterBuilderFactory. También añadiréprivate delante para que sea una propiedad:

... lines 1 - 5
use App\Builder\CharacterBuilderFactory;
... lines 7 - 8
class GameApplication
{
public function __construct(private CharacterBuilderFactory $characterBuilderFactory)
{
}
... lines 14 - 105
}

Luego, dentro de createCharacterBuilder(), en lugar de crear esto a mano, confía en la fábrica: return $this->characterBuilderFactory->createBuilder():

... lines 1 - 8
class GameApplication
{
... lines 11 - 101
private function createCharacterBuilder(): CharacterBuilder
{
return $this->characterBuilderFactory->createBuilder();
}
}

Lo bueno de esta fábrica (y éste es realmente el propósito del patrón de fábrica en general) es que hemos centralizado la instanciación de este objeto.

Introducir los servicios en el constructor

¿Cómo ayuda esto a nuestra situación? Recuerda que el problema que imaginé era el siguiente: ¿Qué pasaría si nuestro constructor de personajes necesitara un servicio como el de EntityManager?

Con nuestra nueva configuración, podemos conseguirlo. En realidad no tengo Doctrine instalado en este proyecto, así que en lugar de EntityManager, vamos a requerirLoggerInterface $logger... y volveré a añadir private delante para convertirlo en una propiedad:

... lines 1 - 14
use Psr\Log\LoggerInterface;
class CharacterBuilder
{
... lines 19 - 23
public function __construct(private LoggerInterface $logger)
{
}
... lines 27 - 96
}

Luego, abajo, en buildCharacter(), sólo para probar que esto funciona, lo utilizaré:$this->logger->info('Creating a character'). También pasaré un segundo argumento con alguna información extra como 'maxHealth' => $this->maxHealth y'baseDamage' => $this->baseDamage:

... lines 1 - 16
class CharacterBuilder
{
... lines 19 - 55
public function buildCharacter(): Character
{
$this->logger->info('Creating a character!', [
'maxHealth' => $this->maxHealth,
'baseDamage' => $this->baseDamage,
]);
... lines 62 - 75
}
... lines 77 - 96
}

CharacterBuilder ahora requiere un $logger... pero CharacterBuilder no es un servicio que vayamos a obtener directamente del contenedor. Lo obtendremos a través deCharacterBuilderFactory, que es un servicio. Así que el autocableado de LoggerInterfacefuncionará aquí:

... lines 1 - 4
use Psr\Log\LoggerInterface;
class CharacterBuilderFactory
{
public function __construct(private LoggerInterface $logger)
{
}
... lines 12 - 16
}

Entonces, pásalo manualmente al constructor como $this->logger:

... lines 1 - 6
class CharacterBuilderFactory
{
... lines 9 - 12
public function createBuilder(): CharacterBuilder
{
return new CharacterBuilder($this->logger);
}
}

Aquí vemos algunas de las ventajas del patrón de fábrica. Como ya hemos centralizado la instanciación de CharacterBuilder, cualquier cosa que necesite unCharacterBuilder, como GameApplication, no necesita cambiar en absoluto... ¡aunque acabemos de añadir un argumento al constructor! GameApplication ya estaba descargando el trabajo de instanciación a CharacterBuilderFactory.

Para ver si esto funciona, ejecuta

./bin/console app:game:play -vv

El -vv nos permitirá ver los mensajes de registro mientras jugamos. Y... ¡lo conseguimos! ¡Mira! Aparece nuestro mensaje[info] Creating a character. No podemos ver las demás estadísticas en esta pantalla, pero están en el archivo de registro. Impresionante.

¿Qué resuelve el patrón constructor?

¡Así que ése es el patrón constructor! ¿Qué problemas puede resolver? Muy sencillo Tienes un objeto que es difícil de instanciar, así que añades una clase constructora para facilitarte la vida. También ayuda con el principio de responsabilidad única. Es una de las estrategias que ayuda a abstraer la lógica de creación de una clase de la clase que utilizará ese objeto. Anteriormente, en GameApplication, teníamos la complejidad tanto de crear los objetos Character como de utilizarlos. Aquí seguimos teniendo código para utilizar el constructor, pero la mayor parte de la complejidad vive ahora en la clase constructora.

¿Necesita mi constructor una interfaz?

A menudo, cuando estudias este patrón, te dirá que el constructor (CharacterBuilder, por ejemplo) debería implementar una nueva interfaz, comoCharacterBuilderInterface, que tendría métodos como setMaxHealth(),setBaseDamage(), etc. Esto es opcional. ¿Cuándo lo necesitarías? Bueno, como todas las interfaces, es útil si necesitas la flexibilidad de cambiar la forma de crear tus personajes por alguna otra implementación.

Por ejemplo, imagina que creamos un segundo constructor que implementaCharacterBuilderInterface llamado DoubleMaxHealthCharacterBuilder. Éste crea objetosCharacter, pero de una forma ligeramente diferente... como si duplicara el $maxHealth. Si ambos constructores implementaranCharacterBuilderInterface, entonces dentro de nuestro CharacterBuilderFactory, que ahora devolvería CharacterBuilderInterface, podríamos leer alguna configuración para averiguar qué clase de CharacterBuilder queremos utilizar.

Así que la creación de esa interfaz realmente tiene menos que ver con el patrón constructor en sí mismo... y más con hacer tu código más flexible. Déjame deshacer ese código falso dentro de CharacterBuilderFactory. Y... dentro de CharacterBuilder, eliminaré esa interfaz falsa.

¿Dónde vemos el patrón constructor?

¿Y dónde vemos el patrón constructor en la naturaleza? Es bastante fácil de detectar porque el encadenamiento de métodos es una característica muy común de los constructores. El primer ejemplo que me viene a la mente es QueryBuilder de Doctrine:

class CharacterRepository extends ServiceEntityRepository
{
    public function findHealthyCharacters(int $healthMin): array
    {
        return $this->createQueryBuilder('character')
            ->orderBy('character.name', 'DESC')
            ->andWhere('character.maxHealth > :healthMin')
            ->setParameter('healthMin', $healthMin)
            ->getQuery()
            ->getResult();
    }
}

Nos permite configurar una consulta con un montón de buenos métodos antes de llamar finalmente a getQuery() para crear realmente el objeto Query. También aprovecha el patrón de fábrica: para crear el constructor, llamas a createQueryBuilder(). Ese método, que vive en la base EntityRepository es la "fábrica" responsable de instanciar el QueryBuilder.

Otro ejemplo es el de Symfony FormBuilder:

public function buildForm(FormBuilderInterface $builder, $options)
{
    $animals = ['🐑', '🦖', '🦄', '🐖'];
    $builder
        ->add('name', TextType::class)
        ->add('animal', ChoiceType::class, [
            'placeholder' => 'Choose an animal',
            'choices' => array_combine($animals, $animals),
        ]);
}

En ese ejemplo, no llamamos al método buildForm(), pero Symfony finalmente sí lo llama una vez que hemos terminado de configurarlo.

Bien equipo, hablemos ahora del patrón observador.

Leave a comment!

4
Login or Register to join the conversation
Kacper-G Avatar
Kacper-G Avatar Kacper-G | posted hace 1 mes

Sorry I had a little trouble with understanding why CharacterBuilderFactory is a service and CharacterBuilder is not.
So in my understanding a typical class is not a service and cannot autowire until it is autowired by another class (in this example GameApplication)?

Reply

Hey Kacper,

The intention of the CharacterBuilderFactory is to allow us to instantiate as many CharacterBuilder objects as we want. In this case, we can't reuse the same instance of a CharacterBuilder because after you use it to create a Character object it holds state (health, damage, armor, etc.), so, instead of clearing the state we just instantiate another builder object. It was done this way for convenience and for quickly talking about the factory pattern.

Cheers!

1 Reply
Sargath Avatar
Sargath Avatar Sargath | posted hace 2 meses | edited

Well, even though CharacterBuilder is not a service, after we add LoggerInterface to its constructor, it will be autowired by a container, as well ass injected by the factory. Shouldn't namespace of the builder be excluded from auto-wiring in the configuration, to actually inject it only by the factory?

Reply

Hey Sargath,

Good question. At this exact moment, it makes sense to exclude that CharacterBuilder from the auto-wiring configuration. Though, further in the course, we will rewrite it into a Symfony service. So, for simplicity, we do not focus on this too much right now, i.e. because eventually, we want it to be a real service in our Symfony app.

But if you want to leave the code as it is in this specific chapter - yeah, you can ignore it which makes more sense :) Though, while you're not using that CharacterBuilder as a service anyway - it's still no that much important ;)

Anyway, a good catch ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!