Login to bookmark this video
Buy Access to Course
35.

Colección escribible mediante el PropertyAccessor

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Para ver lo que ocurre aquí, dirígete al mapeador: UserApiToEntityMapper. La peticiónpatch() tomará estos datos, los rellenará en UserApi... y luego los volveremos a mapear en la entidad de este mapeador.

Y... la razón por la que falla la prueba es bastante obvia: ¡no estamos mapeando la propiedaddragonTreasures del DTO a la entidad!

Vamos a dump($dto) para ver qué aspecto tiene después de deserializar los datos.

53 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 12
class UserApiToEntityMapper implements MapperInterface
{
// ... lines 15 - 34
public function populate(object $from, object $to, array $context): object
{
// ... lines 37 - 46
dump($dto);
// ... lines 48 - 50
}
}

Ejecuta de nuevo la prueba:

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

Y... ¡guau! Los dragonTreasures del DTO siguen siendo los dos originales. Esto me dice que este campo se está ignorando por completo: no se está deserializando. Y apuesto a que sabes la razón. ¡Dentro de UserApi, la propiedad $dragonTreasuresno es writable! Pero está muy bien ver que writable: false hace su trabajo.

70 lines | src/ApiResource/UserApi.php
// ... lines 1 - 42
class UserApi
{
// ... lines 45 - 61
/**
* @var array<int, DragonTreasureApi>
*/
public array $dragonTreasures = [];
// ... lines 66 - 68
}

Cuando volvamos a ejecutar la prueba, verás la diferencia.

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

¡Sí! Sigue habiendo dos tesoros, pero los ID son "1" y "3". Así que UserApi parece correcto.

Pasar de DragonTreasureApi -> DragonTreasure

Ahora, tenemos que tomar esta matriz de objetos DragonTreasureApi y mapearlos a objetos de entidadDragonTreasure para que podamos colocarlos en la entidad User. Una vez más, ¡necesitamos un micro mapeador!

Ya sabes lo que hay que hacer: añade private MicroMapperInterface $microMapper... y vuelve aquí abajo... empieza con $dragonTreasureEntities = []. Yo voy a simplificar las cosas y utilizaré un buen y anticuado foreach. Haz un bucle sobre $dto->dragonTreasures como$dragonTreasureApi. Entonces diremos que $dragonTreasureEntities[] es igual a$this->microMapper->map(), pasando $dragonTreasureApi y DragonTreasure::class. Y como ya habrás adivinado, vamos a pasarMicroMapperInterface::MAX_DEPTH ajustado a 0.

62 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 11
use Symfonycasts\MicroMapper\MicroMapperInterface;
// ... lines 13 - 14
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
// ... lines 18 - 19
private MicroMapperInterface $microMapper,
)
{
}
// ... lines 24 - 37
public function populate(object $from, object $to, array $context): object
{
// ... lines 40 - 50
$dragonTreasureEntities = [];
foreach ($dto->dragonTreasures as $dragonTreasureApi) {
$dragonTreasureEntities[] = $this->microMapper->map($dragonTreasureApi, DragonTreasure::class, [
MicroMapperInterface::MAX_DEPTH => 0,
]);
}
// ... lines 57 - 59
}
}

0 está bien aquí porque sólo tenemos que asegurarnos de que el mapeador del tesoro dragón consulta la entidad DragonTreasure correcta. Si tiene una relación, como owner, no nos importa si ese objeto está totalmente mapeado y poblado. Aquí abajo, dd($dragonTreasureEntities).

62 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 11
use Symfonycasts\MicroMapper\MicroMapperInterface;
// ... lines 13 - 14
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
// ... lines 18 - 19
private MicroMapperInterface $microMapper,
)
{
}
// ... lines 24 - 37
public function populate(object $from, object $to, array $context): object
{
// ... lines 40 - 50
$dragonTreasureEntities = [];
foreach ($dto->dragonTreasures as $dragonTreasureApi) {
$dragonTreasureEntities[] = $this->microMapper->map($dragonTreasureApi, DragonTreasure::class, [
MicroMapperInterface::MAX_DEPTH => 0,
]);
}
dd($dragonTreasureEntities);
// ... lines 58 - 59
}
}

¡Pruébalo!

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

Y... ¡tiene buena pinta! Tenemos 2 tesoros con id: 1... y aquí abajoid: 3.

Llamar a los métodos Sumador/Recuperador

Así que todo lo que tenemos que hacer ahora es colocarlo en el objeto User. Digamos $entity->set... pero... uh oh. ¡No tenemos un método setDragonTreasures()! Y eso es a propósito! Mira dentro de la entidad User. Tiene un método getDragonTreasures(), pero no setDragonTreasures(). En su lugar, tiene addDragonTreasure()y removeDragonTreasure().

No voy a profundizar demasiado en por qué no podemos tener un definidor, pero está relacionado con el hecho de que necesitamos definir el lado propietario de la relación Doctrine. Hablamos de ello en nuestro tutorial sobre las relaciones Doctrine.

La cuestión es que si pudiéramos llamar simplemente a ->setDragonTreasures(), no se guardaría correctamente. Tenemos que llamar a los métodos sumador y eliminador.

¡Y esto es complicado! Tenemos que mirar $dragonTreasureEntities, compararlo con la propiedad actual dragonTreasures, y luego llamar a los sumadores y eliminadores correctos para los tesoros que sean nuevos o eliminados. En nuestro caso, tenemos que llamar aremoveDragonTreasure() para el del medio y a addDragonTreasure() para este tercero.

Escribir este código suena... molesto... y complicado. Afortunadamente, ¡Symfony tiene algo que hace esto! Es un servicio llamado "Property Accessor".

Dirígete aquí... y añade private PropertyAccessorInterface $propertyAccessor.

65 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 9
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
// ... lines 11 - 15
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
// ... lines 19 - 21
private PropertyAccessorInterface $propertyAccessor,
)
{
}
// ... lines 26 - 63
}

Property Accessor es bueno para establecer propiedades. Puede detectar si una propiedad es pública... o si tiene un método establecedor... o incluso métodos sumadores o eliminadores. Para utilizarlo, digamos $this->propertyAccessor->setValue() pasando el objeto al que estamos estableciendo datos - el User $entity , la propiedad que estamos estableciendo -dragonTreasures - y, por último, el valor: $dragonTreasureEntities.

Aquí abajo, vamos a dd($entity) para que veamos cómo queda.

65 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 9
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
// ... lines 11 - 15
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
// ... lines 19 - 21
private PropertyAccessorInterface $propertyAccessor,
)
{
}
// ... lines 26 - 39
public function populate(object $from, object $to, array $context): object
{
// ... lines 42 - 58
$this->propertyAccessor->setValue($entity, 'dragonTreasures', $dragonTreasureEntities);
dd($entity);
// ... lines 61 - 62
}
}

Respira hondo. Inténtalo:

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

Desplázate hacia arriba... hasta el objeto User. ¡Mira dragonTreasures! ¡Tiene dos elementos con id: 1 y id: 3! Ha actualizado correctamente la propiedad dragonTreasures ¿Cómo demonios lo ha hecho? Llamando a addDragonTreasure() para el id 3 y aremoveDragonTreasure() para el id 2.

Puedo demostrarlo. Aquí abajo, añade dump('Removing treasure'.$treasure->getId()).

Cuando volvamos a ejecutar la prueba...

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

¡Ahí está! ¡Eliminando el tesoro 2! La vida es buena. Elimina este dump()... así como el otro de aquí.

Veamos algo de verde. Ejecuta la prueba una última vez... con suerte:

symfony php bin/phpunit --filter=testTreasuresCanBeRemoved

¡Pasa! La respuesta final contiene los tesoros 1 y 3. ¿Qué ha pasado con el tesoro 2? En realidad, se eliminó por completo de la base de datos. Entre bastidores, su propietario se estableció en null. Luego, gracias a orphanRemoval, cada vez que el propietario de uno de estos dragonTreasures se establece en null, se borra. Es algo de lo que ya hablamos en un tutorial anterior.

Antes de seguir adelante, tenemos que limpiar la prueba. Elimina la parte en la que estamos robando $dragonTreasure3. Nos desharemos de ese objeto de ahí, de la parte en la que lo colocamos aquí abajo, cambiaremos la longitud a 1, y sólo probaremos ese. Así que esto ahora sí que es una prueba para eliminar un tesoro.

Celébralo eliminando este ->dump().

115 lines | tests/Functional/UserResourceTest.php
// ... lines 1 - 10
class UserResourceTest extends ApiTestCase
{
// ... lines 13 - 56
public function testTreasuresCanBeRemoved(): void
{
$user = UserFactory::createOne();
$otherUser = UserFactory::createOne();
$dragonTreasure = DragonTreasureFactory::createOne(['owner' => $user]);
DragonTreasureFactory::createOne(['owner' => $user]);
$this->browser()
->actingAs($user)
->patch('/api/users/' . $user->getId(), [
'json' => [
'dragonTreasures' => [
'/api/treasures/' . $dragonTreasure->getId(),
],
],
'headers' => ['Content-Type' => 'application/merge-patch+json']
])
->assertStatus(200)
->get('/api/users/' . $user->getId())
->assertJsonMatches('length("dragonTreasures")', 1)
->assertJsonMatches('dragonTreasures[0]', '/api/treasures/' . $dragonTreasure->getId())
;
}
// ... lines 80 - 113
}

Pero... los tesoros aún se pueden robar, lo cual es lamentable. Arreglemos el validador para esto... pero también hagámoslo mucho más sencillo, gracias al sistema DTO, a continuación.