Colección escribible mediante el PropertyAccessor
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePara 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.
// ... 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 $dragonTreasures
no es writable
! Pero está muy bien ver que writable: false
hace su trabajo.
// ... 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
.
// ... 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)
.
// ... 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
.
// ... 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.
// ... 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()
.
// ... 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.
One more question, patch() should update specific data, no? But it seems to replace those two data in the database ( and remove that second one ) but one gets removed (the second one), I mean why should it get removed due to a patch() request? just because we didn't send it during the update
So, is it removing from the database because when deserializing, it's owner gets set to null, because of the data that's sent ( i.e. dragonTreasures => [ '/api/treasures/1', 'api/treasures/2' ] ) does not get deserialized, and as a result, it's owner is missing ?