Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Hacer preguntas propiedad de los usuarios

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 $12.00

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

Login Subscribe

Nuestro sitio tiene usuarios y estas preguntas son creadas por esos usuarios. Por lo tanto, en la base de datos, cada Question debe estar relacionado con el User que lo creó mediante una relación Doctrine. Ahora mismo, si abres src/Entity/Question.php, ese no es el caso. No hay nada que lo relacione con el User que lo creó. Es hora de arreglar eso. Lo necesitaremos para poder hablar correctamente de los votantes

Generar la relación

Busca tu terminal y ejecuta:

symfony console make:entity

Vamos a modificar la entidad Question y añadir una nueva propiedad llamada owner, que será el "usuario" que posee este Question. Necesitamos una relación ManyToOne. Si alguna vez no estás seguro, escribe "relación" y te guiará a través de un asistente para ayudarte. Será una relación con la clase User... y la propiedad owner no será anulable: cada pregunta debe ser propiedad de algún usuario.

A continuación nos pregunta si queremos mapear el otro lado de la relación para que podamos decir $user->getQuestions(). Eso puede ser útil, así que digamos que sí. Y llamaremos a esa propiedad questions. Por último, voy a decir que no a la eliminación de huérfanos. Y... ¡listo!

Si has pasado por nuestro tutorial sobre las relaciones de Doctrine, sabrás que aquí no hay nada especial. Esto añadió una relación ManyToOne sobre una nueva propiedad $owner... e hizo métodos getter y setter en la parte inferior:

... lines 1 - 16
class Question
{
... lines 19 - 64
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="questions")
* @ORM\JoinColumn(nullable=false)
*/
private $owner;
... lines 70 - 227
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
}

En la clase User, también mapeó el lado inverso de la relación:

... lines 1 - 5
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
... lines 8 - 17
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 20 - 51
/**
* @ORM\OneToMany(targetEntity=Question::class, mappedBy="owner")
*/
private $questions;
public function __construct()
{
$this->questions = new ArrayCollection();
}
... lines 61 - 190
/**
* @return Collection|Question[]
*/
public function getQuestions(): Collection
{
return $this->questions;
}
public function addQuestion(Question $question): self
{
if (!$this->questions->contains($question)) {
$this->questions[] = $question;
$question->setOwner($this);
}
return $this;
}
public function removeQuestion(Question $question): self
{
if ($this->questions->removeElement($question)) {
// set the owning side to null (unless already changed)
if ($question->getOwner() === $this) {
$question->setOwner(null);
}
}
return $this;
}
}

Vamos a hacer una migración para este cambio:

symfony console make:migration

Y... como de costumbre, iremos al nuevo archivo de migración... para asegurarnos de que contiene sólo lo que esperamos. Sí: ALTER TABLE question, añade owner_idy luego las cosas de la clave foránea:

... lines 1 - 12
final class Version20211012184326 extends AbstractMigration
{
... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE question ADD owner_id INT NOT NULL');
$this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494E7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)');
$this->addSql('CREATE INDEX IDX_B6F7494E7E3C61F9 ON question (owner_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494E7E3C61F9');
$this->addSql('DROP INDEX IDX_B6F7494E7E3C61F9 ON question');
$this->addSql('ALTER TABLE question DROP owner_id');
}
}

Arreglar la migración

Vamos a ejecutarlo:

symfony console doctrine:migrations:migrate

Y... ¡falló! No pasa nada. Falla porque ya hay filas en la tabla question. Así que añadir un nuevo owner_id NOT NULL hace que esos registros existentes... exploten. En el tutorial de relaciones de Doctrine, hablamos de cómo manejar, arreglar y probar responsablemente las migraciones fallidas. Como ya hablamos de ello allí, voy a tomar el camino más fácil aquí y simplemente eliminar nuestra base de datos:

symfony console doctrine:database:drop --force

Luego crearé una base de datos nueva:

symfony console doctrine:database:create

Y migrar de nuevo.

symfony console doctrine:migrations:migrate

Ahora ya funciona. Recarga las instalaciones:

symfony console doctrine:fixtures:load

Asignación de propietarios en las instalaciones

Y... ¡eso también explotó! ¡Vamos! La inserción en la pregunta está fallando porqueowner_id no puede ser nula. Eso tiene sentido: aún no hemos entrado en nuestras instalaciones y no hemos asignado un propietario a cada pregunta.

Hagámoslo. Abre src/Factory/QuestionFactory.php. Nuestro trabajo en getDefaults(), es proporcionar un valor por defecto para cada propiedad requerida. Así que ahora voy a añadir una clave owner establecida en UserFactory::new():

... lines 1 - 28
final class QuestionFactory extends ModelFactory
{
... lines 31 - 42
protected function getDefaults(): array
{
return [
... lines 46 - 52
'owner' => UserFactory::new(),
];
}
... lines 56 - 68
}

Gracias a esto, si ejecutamos QuestionFactory sin anular ninguna variable, se creará un nuevo usuario para cada nueva pregunta.

Pero dentro de nuestros accesorios, eso... no es exactamente lo que queremos. Dirígete a la parte inferior, donde creamos los usuarios. Lo que quiero hacer es crear primero estos usuarios. Y luego, cuando creemos las preguntas aquí arriba... oh, en realidad aquí mismo, quiero usar un usuario aleatorio de los que ya hemos creado.

Para ello, primero tenemos que mover nuestros usuarios a la parte superior para que se creen primero:

... lines 1 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
UserFactory::createOne([
'email' => 'abraca_admin@example.com',
'roles' => ['ROLE_ADMIN']
]);
UserFactory::createOne([
'email' => 'abraca_user@example.com',
]);
UserFactory::createMany(10);
TagFactory::createMany(100);
... lines 30 - 61
}
}

Luego, aquí abajo para nuestras preguntas principales, pasar una función al segundo argumento y devolver un array... para que podamos anular la propiedad owner. Ponlo enUserFactory::random():

... lines 1 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 20 - 30
$questions = QuestionFactory::createMany(20, function() {
return [
'owner' => UserFactory::random(),
];
});
... lines 36 - 61
}
}

No me voy a preocupar de hacer esto también para las preguntas no publicadas aquí abajo... pero podríamos.

Bien: probemos de nuevo los accesorios:

symfony console doctrine:fixtures:load

Esta vez... ¡funcionan!

¡Genial! Así que vamos a aprovechar la nueva relación en nuestro sitio para imprimir el propietario real de cada pregunta. También vamos a iniciar una página de edición de preguntas y luego... tendremos que averiguar cómo hacer que sólo el propietario de cada pregunta pueda acceder a ella.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}