I am using api-platform with Symfony 4. It works fine, but I would like to change a GET url from:
/booking/{id} to /booking/{bookingId}
I'm using my own DTO object and custom data provider (not Doctrine ORM).
Here are the current #ApiResource and #ApiProperty definitions that work fine:
/**
*
* #ApiResource(
* itemOperations={
* "get"={
* "path"="/booking/{id}",
* },
* "api_bookings_get_item"={
* "swagger_context"={
* "operationId"="getBookingItem",
* "summary"="Retrieves details on a booking",
* "parameters"= {
* {
* "name"="id",
* "description"="Booking ID",
* "default"="15000",
* "in"="path",
* "required"=true,
* "type"="string"
* }
* },
* "responses"={
* "200"={
* "description"="Results retrieved"
* },
* "404"={
* "description"="Booking not found"
* }
* }
* }
* }
* },
* collectionOperations={}
* )
*/
final class Booking
{
/**
* #var string
* #Assert\NotBlank
*
* #ApiProperty(
* identifier=true,
* attributes={
* "swagger_context"={
* "description"="Booking ID",
* "required"=true,
* "type"="string",
* "example"="123456"
* }
* }
* }
*/
public $id;
// other variables
}
However, if I change all the references from 'id' to 'bookingId' it stops working and I get a 404 error. Here are the changes I made to the above code:
"path"="/booking/{bookingId}"
"name"="bookingId"
public $bookingId;
Is api-platform hard-coded to use 'id' as an identifier? Is there any way to change this?
In Api-platform the id parameter is hardcoded:
namespace ApiPlatform\Core\DataProvider;
private function extractIdentifiers(array $parameters, array $attributes)
{
if (isset($attributes['item_operation_name'])) {
if (!isset($parameters['id'])) {
throw new InvalidIdentifierException('Parameter "id" not found');
}
but you can create your own operation and use the parameter name that you want there is a great example in docs custom operations
You can customize the apiResource identifier via:
/**
* #ApiProperty(identifier=false)
*/
private $id;
and:
/**
* #ApiProperty(identifier=true)
*/
private $uuid;
where uuid is the new identifier to be used in request URLs.
For reference: symfonycasts did an excellent tutorial here:
https://symfonycasts.com/screencast/api-platform-extending/uuid-identifier
Related
I am afraid I might have ran into some sort of XY problem...
I have an entity "Asset" with related "AssetType" entity (One AssetType can have many Asset entities)
When creating new entity with POST method, the request fails with "SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'type_id' cannot be null"
Data posted from react-admin (POST to /api/assets route):
{
"data":{
"type":"assets",
"attributes":{
"name":"asdf",
"description":"LoraWAN enabled sensor"
},
"relationships":{
"asset_type":{
"data":{
"id":"/api/asset_types/a71b47b8-b9fb-11ea-b4d5-e6b986f12daf",
"type":"asset_types"
}
}
}
}
}
I understand that there is data lost somewhere doing deserialization of object, but cannot figure out where. Also I have identical set of entities (Gateway and Location where each Location can have multiple Gateways) and the creation of new entities work as expected...
New to Symfony & api-platform, any help appreciated.
Asset entity is set tup to be visible in api-platform:
/**
* #ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "delete"},
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetRepository")
*/
class Asset
{
/**
* #ORM\Id
* #ORM\Column(type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read"})
*/
private $uuid;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\AssetType", inversedBy="assets", cascade={"persist"})
* #ORM\JoinColumn(name="type_id", referencedColumnName="uuid", nullable=false)
*
* #Groups({"read", "write"})
*/
private $assetType;
}
AssetType entity:
/**
* #ApiResource(
* normalizationContext={"groups"={"read"}},
* denormalizationContext={"groups"={"write"}}
* )
* #ORM\Entity(repositoryClass="App\Repository\AssetTypeRepository")
*/
class AssetType
{
/**
* #ORM\Id()
* #ORM\Column(name="uuid", type="uuid_binary_ordered_time", nullable=false, unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidOrderedTimeGenerator")
* #Groups({"read", "write"})
*/
private $uuid;
/**
* #ORM\Column(type="string", length=255, nullable=true)
* #Groups({"read", "write"})
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Asset", mappedBy="assetType")
*/
private $assets;
public function __construct()
{
$this->assets = new ArrayCollection();
}
public function getUuid()
{
return $this->uuid;
}
public function setUuid($uuid): self
{
$this->uuid = $uuid;
return $this;
}
public function getAssets(): Collection
{
return $this->assets;
}
public function addAsset(Asset $asset): self
{
...
}
public function removeAsset(Asset $asset): self
{
...
}
In case anyone sumbles across similar problem - the reason for missing values was property naming and its normalization.
Relationship data posted contains key "asset_type" which needs to be converted to camelCase "assetType" in react-admin's dataProvider (that's the approach I took).
I'm using JMSSerializer - along with the Doctrine constructor - in order to deserialize an object sent.
My (simplified) entities are the following. I omit the code I think is useless:
Widget
{
protected $id;
/**
* #ORM\OneToMany(
* targetEntity="Belka\Iso50k1Bundle\Entity\VarSelection",
* mappedBy="widget",
* cascade={"persist", "remove", "detach", "merge"})
* #Serializer\Groups({"o-all-getCWidget", "i-p2-create", "o-all-getWidget", "i-p3-create", "i-p2-editWidget"})
* #Type("ArrayCollection<Belka\Iso50k1Bundle\Entity\VarSelection>")
*/
protected $varsSelection;
}
/**
* #ORM\Entity()
*
* #ORM\InheritanceType("SINGLE_TABLE")
*
* #ORM\DiscriminatorColumn(
* name="vartype",
* type="string")
*
* #ORM\DiscriminatorMap({
* "PHY" = "PhyVarSelection"
* })
*
* #ORM\HasLifecycleCallbacks()
*/
abstract class VarSelection
{
/**
* #Id
* #Column(type="integer")
* #GeneratedValue("SEQUENCE")
* #Serializer\groups({"o-all-getCWidget", "o-all-getWidget", "i-p2-editWidget"})
*/
protected $id;
}
class PhyVarSelection extends VarSelection
{
/**
* #var PhyVar
*
* #ORM\ManyToOne(
* targetEntity="Belka\Iso50k1Bundle\Entity\PhyVar",
* cascade={"persist", "merge", "detach"})
*
* #ORM\JoinColumn(
* name="phy_var_sel",
* referencedColumnName="id",
* nullable=false)
*/
protected $phyVar;
}
class PhyVar extends Variable
{
/**
* #ORM\Column(type="string")
* #ORM\Id
*
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget"})
* #Assert\Regex("/(PHY)_\d+_\d+_\w+/")
*/
protected $id;
/**
* #ORM\Column(type="text", name="varname")
* #Serializer\Groups({"o-p2-getCMeters", "o-all-getWidget", "o-all-getCWidget"})
*/
protected $varName;
...
}
I try to deserialize an object that represents a Widget entity already persisted, along with which an array of varselection with their own id specified - if already persisted - and without their own id if they are new and to be persisted.
Deserialization works:
$context = new DeserializationContext();
$context->setGroups('i-p2-editWidget');
$data = $this->serializer->deserialize($content, $FQCN, 'json', $context);
but $data has always Widget::$varsSelection[]::$phyVar as a proxy class initialized, with only the id properly set. What I have to do so as to have it all is:
foreach ($data->getVarsSelection() as $varSel) {
$varSel->getVar();
}
why is that? How can have it initialized already? I don't want to spend time cycling and fetching data from DB again.
edit
I've added a domain of the entities so as to get the idea of what I'm deserializing
I figured out myself the hows and whys of this behavior:
since I'm sending a JSON like the following:
{
"id": <widgetID>,
"vars_selection": {
"id": <varSelectionID>,
"vartype": "PHY"
}
}
JMSSerializer's Doctrine ObjectConstructor simply tries to finds just two Entities: Widget and VarSelection by executing the following line:
$object = $objectManager->find($metadata->name, $identifierList);
in other words: Doctrine's EntityManager tries to find the Entity identified by its ID. Hence, well'get the unitialized proxy classes.
As far as I know, find cannot specify an hydration mode. Hence, two are the ways to handle this:
Specify fetch="EAGER" on PhyVarSelection::$phyVar. Quite costly, when we do not need it though;
Replace the ObjectConstructor by calling the repository and make a DQL, which will have the EAGER option properly set. Something like $query->setFetchMode("PhyVarSelection", "phyVar", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
I use Nelmio to generate automatically my api doc. I would like to return an object in responseMap which is a simple class (entity without a database associated) like this :
/**
* #ApiDoc(
* description = "Get informations from user.",
* responseMap = {
* 200 = { "\AppBundle\Entity\MyUserInfos" },
* },
* )
*
* #Rest\View(statusCode=Response::HTTP_OK)
* #Rest\Get("/my_user_infos")
*/
public function getMyUserInfosAction(Request $request) {
...
}
namespace AppBundle\Entity;
/**
* MyUserInfos
*/
class MyUserInfos
{
/**
* #var string
*/
private $username;
/**
* #var string
*/
private $email;
+getters and setters
}
But response object is not displayed in my api doc. Can anyone help me ?
Thanks.
Remove the leading backslash on you class name
/**
* #ApiDoc(
* description = "Get informations from user.",
* responseMap = {
* 200 = { "AppBundle\Entity\MyUserInfos" },
* },
* )
*
* #Rest\View(statusCode=Response::HTTP_OK)
* #Rest\Get("/my_user_infos")
*/
public function getMyUserInfosAction(Request $request) {
...
}
See:https://github.com/nelmio/NelmioApiDocBundle/blob/2d70b0802144fd2c868783c46fa1be4a774967d4/Resources/doc/swagger-support.rst#multiple-response-models
Serialization groups work for me fine when i fetch one entity but
when i try fetch array of results i got empty result set.
I do this like that:
* #\FOS\RestBundle\Controller\Annotations\View(serializerGroups={"client"})
but also tried manually set context on view object, and same situation.
When i don't set group or set to "Default":
* #\FOS\RestBundle\Controller\Annotations\View(serializerGroups={"Default"})
i got proper serialized result set.
My entity:
use JMS\Serializer\Annotation as JMS;
[...]
/**
* Document
*
* #ORM\Table(name="documents")
* #ORM\Entity(repositoryClass="AppBundle\Entity\DocumentRepository")
* #ORM\EntityListeners({"DocumentListener"})
* #JMS\ExclusionPolicy("all")
* #JMS\AccessorOrder("custom", custom = {"id", "folderId", "title"})
*/
class Document implements ResourceInterface
{
/**
* #var integer
*
* #ORM\Column(type="integer", name="id")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #JMS\Groups({"client"})
* #JMS\Expose()
*/
protected $id;
/**
* #var string
*
* #ORM\Column(type="text", name="description")
* #JMS\Groups({"client"})
* #JMS\Expose()
*/
protected $description;
[...]
and my controller:
// not working
/**
* #\FOS\RestBundle\Controller\Annotations\View(serializerGroups={"client"})
* #return Paginator
* #Get("/documents")
*/
public function documentsAction(ParamFetcher $params)
{
[...]
return ($this->getPaginator(
$params,
$documentBundle->get()
));
}
//works fine
/**
* Get document
*
* #QueryParam(name="id", requirements="\d+", nullable=false)
* #param ParamFetcher $params
* #\FOS\RestBundle\Controller\Annotations\View(serializerGroups={"client"})
*/
public function documentAction(ParamFetcher $params)
{
/** #var DocumentRepository $documentBundle */
$documentBundle = $this->getDoctrine()->getRepository('AppBundle:Document');
return $documentBundle->findByDocumentId($params->get('id'));
}
PS.
fosrestbundle 1.7.9
jmsserializer 0.16.0
I am using nelmioapidocbundle in order to documents my Rest API built on the top of symfony-2.x.
I can't found the right annotation to use to show each Entity's property description on the return section (Please see bellow attached image).
My Entity :
/**
* Checkins
*
* #ORM\Table(name="CheckIns")
* #ORM\Entity(repositoryClass="Project1\ApiBundle\Entity\CheckinsRepository")
*
* #ExclusionPolicy("none")
*/
class Checkins
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #Groups({"checkin"})
* #
*/
private $id;
My Controller :
class CheckinController extends BaseRestController
{
/**
* #ApiDoc(
* resource=true,
* description="Find checkin by ID",
*
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="checkin id"}
* }
*
* output={
* "class"="Project1\ApiBundle\Entity\Checkins",
* "groups"={"checkin"}
* },
* statusCodes={
* 200="Checkin found",
* 400="ID is required",
* 404="Checkin not found"
* }
* )
*
* #Rest\View()
*/
public function getAction(Request $request)
{}
Result ( Description column is empty ) :
There's a description in doc section of the bundle:
For classes parsed with JMS metadata, description will be taken from the properties doc comment, if available.
For Form Types, you can add an extra option named description on each
field
Please visit the following link for more instructions(info at the bottom of the section):
https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md#the-apidoc-annotation