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 SubscribeCierra la operación POST. Quiero hacer una petición GET a la colección de usuarios. Veamos aquí - el usuario con id 4 tiene un CheeseListing
adjunto - id 2. Ok, cierra esa operación y abre la operación para PUT
: Quiero editar ese usuario. Introduce el 4 como id.
En primer lugar, voy a hacer algo que ya hemos visto: vamos a actualizar el campocheeseListings
: establécelo como una matriz con un IRI dentro: /api/cheeses/2
. Si no hiciéramos nada más, esto establecería esta propiedad como... exactamente lo que ya es: el id de usuario 4 ya tiene este CheeseListing
.
Pero ahora, añade otro IRI: /api/cheeses/3
. Que ya existe, pero es propiedad de otro usuario. Cuando pulso Execute.... pfff - me sale un error de sintaxis, porque me he dejado una coma de más en mi JSON. Boo Ryan. Vamos a... intentarlo de nuevo. Esta vez... ¡bah! Un código de estado 400:
Este valor no debería estar en blanco
¡Mis experimentos con la validación acaban de volverse en mi contra! Hemos puesto el title
deCheeseListing
3 en una cadena vacía en la base de datos... es básicamente un registro "malo" que se coló cuando jugábamos con la validación incrustada. Podríamos arreglar ese título... o... simplemente cambiar esto por /api/cheeses/1
. ¡Ejecutar!
Esta vez, ¡funciona! Pero, no es una sorpresa, ¡básicamente ya lo hemos hecho! Internamente, el serializador ve el IRI existente de CheeseListing
- /api/cheeses/2
, se da cuenta de que ya está establecido en nuestro User
, y... no hace nada. Es decir, quizá vaya a tomar un café o a dar un paseo. Pero, definitivamente, no llama a$user->addCheeseListing()
... ni hace realmente nada. Pero cuando ve el nuevo IRI - /api/cheeses/1
, se da cuenta de que este CheeseListing
no existe todavía en el User
, y entonces, sí llama a $user->addCheeseListing()
. Por eso son tan útiles los métodos de adición y eliminación: el serializador es lo suficientemente inteligente como para llamarlos sólo cuando realmente se está añadiendo o eliminando un objeto.
Ahora, hagamos lo contrario: imagina que queremos eliminar un CheeseListing
de este User
- eliminar /api/cheeses/2
. ¿Qué crees que ocurrirá? Ejecuta y... ¡woh! ¡Un error de restricción de integridad!
Se ha producido una excepción al ejecutar UPDATE cheese_listing SET owner_id=NULL - la columna
owner_id
no puede ser nula.
¡Esto es genial! El serializador se ha dado cuenta de que hemos eliminado el CheeseListing
con id = 2. Y así, llamó correctamente a $user->removeCheeseListing()
y le pasó aCheeseListing
el id 2. Entonces, nuestro código generado estableció el propietario en ese CheeseListing
como nulo.
Dependiendo de la situación y de la naturaleza de la relación y las entidades, ¡esto podría ser exactamente lo que quieres! O, si se tratara de una relación ManyToMany, el resultado de ese código generado sería básicamente "desvincular" los dos objetos.
Pero en nuestro caso, no queremos que un CheeseListing
sea nunca "huérfano" en la base de datos. De hecho... ¡es exactamente por lo que hicimos owner
nullable=false
y por lo que vemos este error! No, si se elimina un CheeseListing
de un User
... ¡supongo que tenemos que eliminar ese CheeseListing
por completo!
Y... ¡sí, hacer eso es fácil! Todo el camino de vuelta por encima de la propiedad $cheeseListings
, añade orphanRemoval=true
.
... lines 1 - 22 | |
class User implements UserInterface | |
{ | |
... lines 25 - 58 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\CheeseListing", mappedBy="owner", cascade={"persist"}, orphanRemoval=true) | |
... lines 61 - 62 | |
*/ | |
private $cheeseListings; | |
... lines 65 - 185 | |
} |
Esto significa que, si alguno de los CheeseListings
de esta matriz de repente... no está en esta matriz, Doctrine lo borrará. Simplemente, ten en cuenta que si intentas reasignar un CheeseListing
a otro User
, seguirá borrando eseCheeseListing
. Así que asegúrate de que sólo utilizas esto cuando no sea un caso de uso. Hemos cambiado el propietario de los listados de queso un montón... pero sólo como ejemplo: no tiene realmente sentido, así que esto es perfecto.
Ejecuta una vez más. Funciona... y sólo está /api/cheeses/1
. Y si volvemos a buscar la colección de listados de quesos... sí,CheeseListing
id 2 ha desaparecido.
A continuación, cuando combinas las relaciones y el filtrado... bueno... obtienes una potencia bastante importante.
Hey Jakub!
Sorry for the very slow reply! Apparently I'm still catching up from Symfony's conference week :p.
I'm not familiar with this error. However, I can see that this class has been modified in later versions. For example, str_replace()
exists in this class in API Platform 2.4 and 2.5, but not in 2.6: they may have fixed some bugs or find a better way to do something. I would try upgrading if you can. If you can't, the problem is on this line - https://github.com/api-platform/core/blob/1a811560d55c5f479fddcc8b7b0f7b36b6e734aa/src/Bridge/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php#L155 - I'm not sure what it is, but something is going wrong with getting information about how to join to make this filter.
Cheers!
Hello,
Am trying to remove an 'attribute' IRI but i got an error Invalid IRI, its a ManyToOne
{
"@id": "/api/document_attribute_values/1",
"@type": "DocumentAttributeValue",
"id": 2,
"attribute":"",
"document": "/api/documents/1",
"lang": "/api/langs/1",
"createdBy": "/api/users/1",
"createdAt": "2021-04-28T09:32:48+02:00",
"label": "test"
Thanks in Advance.
Hey @Gab!
Hmm. Can you add some more information? What is the URL, method and data for the request that you're sending? What does the class look like that has the ManyToOne? I'm... not sure I understand the situation yet :).
Cheers!
This is object in post method :
DocumentAttributeValue.jsonld{
document string($iri-reference)
attribute string($iri-reference)
lang string($iri-reference)
createdBy string($iri-reference)
createdAt string($date-time)
label string
}
Hey @Gab!
Sorry for the VERY slow reply - it was a particularly busy week - my apologies.
Hmm. I still don't understand. I think what you posted here is a description of part of the API - from maybe the API docs/homepage? You originally said:
Am trying to remove an 'attribute' IRI but i got an error Invalid IRI
I was curious exactly what URL you are hitting and what data you are sending. For example, you might be making a request like this:
POST /api/documents/1
{
"document_attribute_value": "/api/document_attribute_values/5"
}
This is almost definitely not correct - I am totally guessing. But this is the format I'm hoping to see: what is the URL and JSON data you are sending. Also what is the exact error you get back?
Cheers!
Hi there,
It works well in RESTFul api. I'd like to know if there is a way to handle adding and removing element from ManyToMany collection using Graphql? I tried to use updateResource but it seems that the property always got overridden. Thanks.
Hi Tianyu W.!
Sorry for the slow reply! Unfortunately, none of us on the team have worked with GraphQL, so we don't know the answer here :/. I would expect it to be possible, but I don't know for sure. Just keep in mind that the system works (and I'm almost positive this is true with GraphQL) by calling getter, setter, adder and remover methods on your object. So if you can get API Platform to call the right method... and that method does it's job, it "should" work. But... this is a very high level answer. Sorry I can't do better!
Cheers!
Hi Ryan,
Is it a good practice to allow nullable setter for a not null field? For example, the $owner property in this case, while it is a not null column in database, we need to allow setter and getter nullable, to allow orphanRemoval=true
feature can be workable. For me, it looks like a workaround approach. I'm not sure any better approach.
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="cheeseListings")
* @ORM\JoinColumn(nullable=false)
* @Assert\Valid()
*
* @Groups({"cheese_listing:read","cheese_listing:write"})
*/
private ?User $owner;
public function getOwner(): ?User
{
return $this->owner;
}
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
Hey Tuancode,
Yes, unfortunately, you need to allow null for setters/getters even if you do not allow nullable fields in the database. That's because an entity might be in an invalid state and Symfony validator could work properly. So yes, it's not perfect but practical thing to do. Another way probably to use Data Transfer Objects (DTO) instead where you will allow null in setters/getters for such fields, they might be in an invalid state, and when they pass validation - map all their values to the specific entity and when you will do it - you will be sure that all required not nullable fields will be set correctly. But allowing null in entities just easier and less extra work, and fairly speaking I'm not sure 100% this approach with DTO will work with APiPlatform, probably you would need to write more custom code for this.
Cheers!
Hi,
I think there is some kind of bug in swagger-ui or api platform.
The schema "cheeses:jsonld-write" does not show that there is an owner property on cheeses.
The thing is the owner property actually exists and works, but swagger does not show it.
Here are some images describing this better:
some more info:
The owner property is annotated like this:
`
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="cheeseListings")
* @ORM\JoinColumn(nullable=false)
* @Groups({"cheese_listing:read", "cheese_listing:write"})
* @Assert\Valid
*/
private $owner;
`
Edit: It seems the problem was bad named "swagger_definition_name" on user. Instead of naming it Read and Write I named it User-read and User-Write which fixed it. Kinda really strange how renaming the schema can have such a side effect.
Hey Daniel W.!
That *is* strange... I'm not super familiar with this area so I'm not sure *why* that would be the case.
Cheers!
Having a hard time wrapping my head around how one would leverage reassigning ownership of an entity, like being done here, in an application that has more strict ownership rules.
For example:
If we had 3 entities (User, UserGroup and CheeseListing) where users should not be able to interact with cheese listings that are outside of their user's user group - how would one apply those rules? As it stands now, a user could be assigned any cheese listing - instead of only being able to be assigned a subset of cheese listings in which it should have access. I've been trying to search the docs to figure this out but not coming up with any right way to implement this. I see mention of using access control to limit access to sub-resources (https://api-platform.com/do... but if that is limited to roles and such, and a user could have the same role across user groups, I'm not sure how to approach the problem.
Any suggestions are appreciated. Figure this is probably a pretty common situation so perhaps I am just overlooking something. My mind goes to multi-tenant applications where this would be needed on most any related resource.
Was thinking about this last night and I guess you could just use custom validation constraints to handle this - which I believe would be the most appropriate way to handle this.
Hey Eric,
Good question! I think you're on the right direction here, probably using custom validation constraints in this case would be the most flexible and powerful solution. Nothing much except this can advise here :/
Cheers!
Hey victor !
Cool question :). We've just started releasing our security tutorial - https://symfonycasts.com/screencast/api-platform-security - and this is exactly the kind of stuff I hope to clear up there. But, let me answer now and we can make sure that I'm answering everything clearly :).
There are two parts to this that I can see:
1) How do I restrict access (e.g. GET) to a specific CheeseListing so that only a User that belongs to a UserGroup that owns a CheeseListing can access it?
First, don't use sub resources :). I just wanted to mention that first - because it can complicate things a bit (as you might successfully secure the GET operation for cheese listings, but forget that you've exposed cheeseListings as a sub-resource of some other entity... or something like that ;).
To solve this problem, you'll use two things: (A) access_control (https://api-platform.com/docs/core/security/#configuring-the-access-control-message) with voters. So, I might use something like access_control"="is_granted('READ', previous_object)"
. Then you would have a custom voter that is able to decide whether or not the current user has "READ" access (I just invented that string) to this CheeseListing (that's what "previous_object" represents, and is passed to your voter - it is the CheeseListing object before it was modified by the request). And (B) you will probably need a custom Doctrine extension (https://api-platform.com/docs/core/extensions/#custom-doctrine-orm-extension) so that you can also filter the collection resource by this same logic (so that when you make a GET collection to /api/cheeses, you only see what you should see).
2) How can I change ownership of a CheeseListing?
I think this might also be part of your question. In your model, changing ownership would mean that you're changing, for example, the "group" property on a CheeseListing. Other than security, that's trivial: you're just updating a property on CheeseListing. But to prevent a "bad" user from doing this (e.g. to prevent someone from changing the CheeseListing from some OTHER group to their OWN group) you would leverage access_control once again - the previous_object variable I mention above will contain the CheeseListing.group property before it was changed. So, naturally, your voter logic will see if the user making this request belongs to the group of this CheeseListing or not.
A similar question to 2 is: what if they "pass" my access_control successfully (they DO have access) but then I need to make sure that the new CheeseListing.group is a value that they are allowed to use? For example, suppose i CAN access this CheeseListing, but I'm trying to change its group to some group that I'm not part of. That should not be allowed. The answer to this is indeed a custom validation constraint.
Let me know if this helps! There are about 4 different subtle different "ways" to protect a resource (protect entire resource/operation, change fields based on the user, prevent bad data from being set, etc) and each has a specific solution. This will all be in the security tutorial :).
Cheers!
In a real situation nobody will remove the orphan, instead I want to set another field like isRemoved=1. Is there a way to do that or we have to update this field separately?
Hey Rakodev
I'm not sure but I would say yes, you have to remove the "orphan" config and do the update yourself. But it seems to me that what you want is something similar as the "SoftDeleteable" extension. Give it a check and decide if it fits your needs or not (https://github.com/Atlantic... )
Cheers!
Hey MolloKhan ,
Thank you for the suggestion. Finally I did something by myself instead of using another vendor.
My solution is this one, and it works:
https://gist.github.com/rak...
If anyone try it, I will appreciate any feedback.
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.17.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}
Hello again,
now I have problem with
owner.username
filter. When I try to filter cheese listings by owner username the response code is 500 and the error message is:I don't know why this function is given a null argument instead something to be replaced.
I'm using Symfony 4.4.32 and OpenAPI 3.0.2.
I would be grateful for reply.
Jakub