Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Añadir tipos de propiedades a las entidades

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

Una nueva característica se coló en Doctrine hace un tiempo, y es súper genial. Ahora Doctrine puede adivinar alguna configuración sobre una propiedad a través de su tipo. Empezaremos con las propiedades de relación. Pero antes, quiero asegurarme de que mi base de datos está sincronizada con mis entidades. Ejecuta:

symfony console doctrine:schema:update --dump-sql

Y... ¡sí! Mi base de datos sí se parece a mis entidades. Volveremos a ejecutar este comando más tarde, después de hacer un montón de cambios... porque nuestro objetivo no es realmente cambiar nada de la configuración de nuestra base de datos: sólo simplificarla. Ah, y sí, esto ha volcado un montón de depreciaciones... las arreglaremos... eventualmente... ¡Lo prometo!

Eliminación de targetEntity

Este es el cambio número uno. Esta propiedad question contiene un objeto Question. Así que vamos a añadir un tipo Question. Pero tenemos que tener cuidado. Tiene que ser un Question anulable. Aunque sea necesario en la base de datos, después de instanciar el objeto, la propiedad no se rellenará instantáneamente: al menos temporalmente, no se establecerá. Verás que hago esto con todos mis tipos de propiedades de entidad. Si es posible que una propiedad sea null -aunque sea por un momento- debemos reflejarlo.

... lines 1 - 9
class Answer
{
... lines 12 - 33
private ?Question $question = null;
... lines 35 - 120
}

También voy a inicializar esto con = null. Si eres nuevo en los tipos de propiedad, esto es lo que pasa. Si añades un tipo a una propiedad... y luego intentas acceder a ella antes de que esa propiedad se haya establecido en algún valor, obtendrás un error, como

No se puede acceder a la propiedad de tipo Answer::$question antes de la inicialización.

Sin un tipo de propiedad, el = null no es necesario, pero ahora sí. Gracias a esto, si instanciamos un Answer y luego llamamos a getQuestion() antes de que se establezca esa propiedad, las cosas no explotarán.

Vale, añadir tipos de propiedades está bien: hace que nuestro código sea más limpio y ajustado. Pero hay otra gran ventaja: ¡ya no necesitamos el targetEntity! Ahora Doctrine es capaz de resolverlo por nosotros. Así que borra esto... ¡y celébralo!

... lines 1 - 9
class Answer
{
... lines 12 - 31
#[ORM\ManyToOne(inversedBy: 'answers')]
... line 33
private ?Question $question = null;
... lines 35 - 120
}

Entonces... sigue yendo a Question. Estoy buscando específicamente campos de relación. Éste es un OneToMany, que contiene una colección de $answers. Vamos a añadir un tipo aquí... pero en un minuto. Centrémonos primero en las relaciones de ManyToOne.

Aquí abajo, en owner, añade ?User, $owner = null, y luego deshazte de targetEntity.

... lines 1 - 13
class Question
{
... lines 16 - 47
#[ORM\ManyToOne(inversedBy: 'questions')]
... line 49
private ?User $owner = null;
... lines 51 - 219
}

Y luego en QuestionTag, haz lo mismo: ?Question $question = null... y da la vuelta de la victoria eliminando targetEntity.

... lines 1 - 8
class QuestionTag
{
... lines 11 - 15
#[ORM\ManyToOne(inversedBy: 'questionTags')]
... line 17
private ?Question $question = null;
... lines 19 - 71
}

Y... aquí abajo... ¡una vez más! ?Tag $tag = null... y despídete detargetEntity.

... lines 1 - 8
class QuestionTag
{
... lines 11 - 19
#[ORM\ManyToOne()]
... line 21
private ?Tag $tag = null;
... lines 23 - 71
}

¡Qué bien! Para asegurarnos de que no hemos estropeado nada, vuelve a ejecutar el comando schema:update de antes:

symfony console doctrine:schema:update --dump-sql

Y... ¡todavía estamos bien!

Añadir tipos a todas las propiedades

Bien, vayamos más allá y añadamos tipos a todas las propiedades. Esto supondrá más trabajo, pero el resultado merece la pena. En el caso de $id, será un int anulable... y lo inicializaremos a null. Gracias a ello, no necesitamos type: 'integer': Doctrine ya puede resolverlo.

... lines 1 - 9
class Answer
{
... lines 12 - 19
#[ORM\Column()]
private ?int $id = null;
... lines 22 - 120
}

Para $content, una cadena anulable... con = null. Pero en este caso, sí necesitamos mantener type: 'text'. Cuando Doctrine ve el tipo string, adivinatype: 'string'... que contiene un máximo de 255 caracteres. Como este campo contiene mucho texto, anula la suposición con type: 'text'.

... lines 1 - 9
class Answer
{
... lines 12 - 22
#[ORM\Column(type: 'text')]
private ?string $content = null;
... lines 25 - 120
}

¿Inicializa el campo de la cadena como nulo o ''?

Por cierto, algunos os preguntaréis por qué no uso $content = ''en su lugar. Diablos, ¡entonces podríamos eliminar la nulidad de ? en el tipo! ¡Es una buena pregunta! La razón es que este campo es obligatorio en la base de datos. Si inicializamos la propiedad a comillas vacías... y tengo un error en mi código por el que me olvidé de establecer la propiedad $content, se guardaría con éxito en la base de datos con el contenido establecido en una cadena vacía. Al inicializarlo a null, si nos olvidamos de establecer este campo, explotará antes de entrar en la base de datos. Entonces, podemos arreglar ese error... en lugar de que guarde silenciosamente la cadena vacía. Puede que sea furtivo, pero nosotros lo somos más.

Bien, ¡continuemos! Gran parte de esto será un trabajo intenso... así que avancemos lo más rápido posible. Añade el tipo a username... y elimina la opción Doctrina type. También podemos eliminar length... ya que el valor por defecto siempre ha sido 255. La propiedad $votesse ve bien, pero podemos deshacernos de type: 'integer'. Y aquí abajo para $status, esto ya tiene el tipo, así que elimina type: 'string'. Pero tenemos que mantener el length si queremos que sea más corto que el 255.

... lines 1 - 9
class Answer
{
... lines 12 - 25
#[ORM\Column()]
private ?string $username = null;
#[ORM\Column()]
private int $votes = 0;
... lines 31 - 35
#[ORM\Column(length: 15)]
private string $status = self::STATUS_NEEDS_APPROVAL;
... lines 38 - 120
}

Pasamos a la entidad Question. Dale a $id el tipo... elimina su opción type Doctrina, actualiza $name... elimina todas sus opciones.... y repite esto para $slug. Observa que $slug todavía utiliza una anotación de @Gedmo\Slug. Lo arreglaremos en un minuto.

Actualiza $question... y luego $askedAt. Esto es un type: 'datetime', así que va a contener una instancia de ?\DateTime. También la inicializaré a null. Ah, y me olvidé de hacerlo, pero ahora podemos eliminar type: 'datetime'.

... lines 1 - 13
class Question
{
... lines 16 - 19
#[ORM\Column()]
private ?int $id = null;
#[ORM\Column()]
private ?string $name = null;
/**
* @Gedmo\Slug(fields={"name"})
*/
#[ORM\Column(length: 100, unique: true)]
private ?string $slug = null;
#[ORM\Column(type: 'text')]
private ?string $question = null;
#[ORM\Column(nullable: true)]
private ?\DateTime $askedAt = null;
... lines 37 - 219
}

Tipificación de las propiedades de la colección

Y ahora volvemos a la relación OneToMany. Si miras hacia abajo, esto se inicializa en el constructor a un ArrayCollection. Así que podrías pensar que deberíamos usar ArrayCollection para el tipo. Pero en su lugar, digamos Collection.

Esa es una interfaz de Doctrine que implementa ArrayCollection. Tenemos que utilizar Collection aquí porque, cuando busquemos un Question en la base de datos y obtengamos la propiedad $answers, Doctrine la establecerá en un objeto diferente: un PersistentCollection. Así que esta propiedad puede ser un ArrayCollection, o un PersistentCollection... pero en todos los casos, implementará esta interfazCollection. Y esto no necesita ser anulable porque se inicializa dentro del constructor. Haz lo mismo con $questionTags.

... lines 1 - 13
class Question
{
... lines 16 - 42
private Collection $answers;
... lines 44 - 45
private Collection $questionTags;
... lines 47 - 219
}

Aunque no lo creas, ¡estamos en la recta final! En QuestionTag... haz nuestros cambios habituales en $id... y luego baja a $taggedAt. Este es un tipo datetime_immutable, así que utiliza \DateTimeImmutable. Fíjate en que no lo he hecho anulable y no lo estoy inicializando a null. Eso es simplemente porque lo estamos estableciendo en el constructor. Así nos garantizamos que siempre contendrá una instancia de\DateTimeImmutable: nunca será nula.

... lines 1 - 8
class QuestionTag
{
... lines 11 - 12
#[ORM\Column()]
private ?int $id = null;
... lines 15 - 23
#[ORM\Column()]
private \DateTimeImmutable $taggedAt;
... lines 26 - 71
}

Bien, ahora a Tag. Haz nuestro habitual baile de $id. Pero espera... en QuestionTag, me olvidé de quitar el type: 'integer'. No hace nada... simplemente no es necesario. Y... lo mismo para type: 'datetime_immutable.

De vuelta en Tag, sigamos con la propiedad $name... esto es todo normal...

... lines 1 - 9
class Tag
{
... lines 12 - 15
#[ORM\Column()]
private ?int $id = null;
#[ORM\Column()]
private ?string $name = null;
... lines 21 - 37
}

Luego salta a nuestra última clase: User. Aceleraré los aburridos cambios en $id y $email... y $password. Eliminemos también el PHP Doc de @var que está por encima de éste: ahora es totalmente redundante. Hagamos lo mismo con $plainPassword. Diablos, este @var ni siquiera estaba bien - ¡debería haber sido string|null!

Vamos a hacer un acercamiento a los últimos cambios: $firstName, añade Collection a$questions... y no hace falta type para $isVerified.

... lines 1 - 13
class User implements UserInterface
{
... lines 16 - 17
#[ORM\Column()]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
... lines 23 - 29
#[ORM\Column(type: 'string')]
private ?string $password = null;
/**
* Non-mapped field
*/
private ?string $plainPassword = null;
#[ORM\Column()]
private ?string $firstName = null;
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'owner')]
private Collection $questions;
#[ORM\Column(type: 'boolean')]
private bool $isVerified = false;
... lines 46 - 210
}

Y... ¡hemos terminado! Esto ha sido una faena. Pero en adelante, el uso de tipos de propiedades significará un código más ajustado... y menos configuración de Doctrine.

Pero... veamos si hemos estropeado algo. Ejecuta doctrine:schema:update por última vez:

symfony console doctrine:schema:update --dump-sql

¡Está limpio! Hemos cambiado una tonelada de configuración, pero en realidad no ha cambiado cómo se mapea ninguna de nuestras entidades. Misión cumplida.

Actualización de la anotación Gedmo\Slug

Ah, y como prometimos, hay una última anotación que tenemos que cambiar: está en la entidad Question, encima del campo $slug. Proviene de la biblioteca de extensiones de Doctrine. El rector no lo ha actualizado... pero es súper fácil. Siempre que tengas Doctrine Extensions 3.6 o superior, puedes utilizarlo como atributo. Así que#[Gedmo\Slug()] con una opción fields que tenemos que establecer en un array. Lo bueno de los atributos PHP es que... ¡sólo son código PHP! Así que escribir un array en atributos... es lo mismo que escribir un array en PHP. Dentro, pasa 'name'... usando comillas simples, como solemos hacer en PHP.

... lines 1 - 9
use Gedmo\Mapping\Annotation as Gedmo;
... lines 11 - 13
class Question
{
... lines 16 - 25
#[Gedmo\Slug(fields: ['name'])]
#[ORM\Column(length: 100, unique: true)]
private ?string $slug = null;
... lines 29 - 217
}

Bien, equipo: acabamos de dar un gran paso adelante en nuestro código base. A continuación, vamos a centrarnos en las desaprobaciones restantes y a trabajar para aplastarlas. Vamos a empezar con el elefante en la habitación: la conversión al nuevo sistema de seguridad. Pero no te preocupes Es más fácil de lo que crees

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.6", // v3.6.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.5
        "doctrine/annotations": "^1.13", // 1.13.2
        "doctrine/dbal": "^3.3", // 3.3.5
        "doctrine/doctrine-bundle": "^2.0", // 2.6.2
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.0", // 2.11.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
        "knplabs/knp-time-bundle": "^1.18", // v1.18.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.6
        "sentry/sentry-symfony": "^4.0", // 4.2.8
        "stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.7
        "symfony/console": "6.0.*", // v6.0.7
        "symfony/dotenv": "6.0.*", // v6.0.5
        "symfony/flex": "^2.1", // v2.1.7
        "symfony/form": "6.0.*", // v6.0.7
        "symfony/framework-bundle": "6.0.*", // v6.0.7
        "symfony/mailer": "6.0.*", // v6.0.5
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/property-access": "6.0.*", // v6.0.7
        "symfony/property-info": "6.0.*", // v6.0.7
        "symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
        "symfony/routing": "6.0.*", // v6.0.5
        "symfony/runtime": "6.0.*", // v6.0.7
        "symfony/security-bundle": "6.0.*", // v6.0.5
        "symfony/serializer": "6.0.*", // v6.0.7
        "symfony/stopwatch": "6.0.*", // v6.0.5
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-chartjs": "^2.0", // v2.1.0
        "symfony/validator": "6.0.*", // v6.0.7
        "symfony/webpack-encore-bundle": "^1.7", // v1.14.0
        "symfony/yaml": "6.0.*", // v6.0.3
        "symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/string-extra": "^3.3", // v3.3.5
        "twig/twig": "^2.12|^3.0" // v3.3.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
        "phpunit/phpunit": "^9.5", // 9.5.20
        "rector/rector": "^0.12.17", // 0.12.20
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/maker-bundle": "^1.15", // v1.38.0
        "symfony/var-dumper": "6.0.*", // v6.0.6
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.6
        "zenstruck/foundry": "^1.16" // v1.18.0
    }
}