Doctrine Extension with API Platform - SQL - php

I'm working on some API Platform project.
I 'm using my API point /references to get some data containing subentities.
But the issue is that I use a Doctrine extension to filter my list. Using this extension I can filter which references I get.
But in the subentity Stock I stil have some data that I dont want.
My goal is to get all stocks that have the warehouse.owner = $owner.
Here is the code :
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
if ($this->security->isGranted('SCOPE admin') || !$this->security->isGranted('SCOPE logistician')) {
return;
}
/** #var Client $client */
$client = $this->security->getUser();
$rootAlias = $queryBuilder->getRootAliases()[0];
switch ($resourceClass) {
case Entry::class:
case Inventory::class:
$queryBuilder->join(sprintf('%s.stock', $rootAlias), 's');
break;
case Reference::class:
// I'm here
$queryBuilder->join(sprintf('%s.stocks', $rootAlias), 's');
break;
default:
return;
break;
}
$queryBuilder->join('s.warehouse', 'w', Expr\Join::WITH, 'w.owner = :owner');
$queryBuilder->setParameter('owner', $client->getSubject());
}
Here is the response :
{
"#id": "/stock/references/7a00e32f-1195-43e6-a5d1-f75d64471112",
"#type": "Reference",
"id": "7a00e32f-1195-43e6-a5d1-f75d64471112",
"quantityAvailable": 75,
"quantitiesOnHand": 100,
"quantityOfExpectedEntries": -25,
"archived": false,
"stocks": [
{
"#id": "/stock/stocks/214d9b27-d2c8-45e5-9d67-10985291022a",
"#type": "Stock",
"quantityOnHand": 50,
"lastCountedEntryWasCreatedAt": "2020-07-23T10:04:32+02:00",
"warehouse": {
"#id": "/stock/warehouses/3a61275f-4b20-4061-a64e-52783cf4d892",
"#type": "Warehouse",
"id": "3a61275f-4b20-4061-a64e-52783cf4d892",
"owner": "9001",
"name": "Adams-Reichel",
"createdAt": "2020-07-23T10:04:32+02:00"
},
"createdAt": "2020-07-23T10:04:32+02:00"
},
{
"#id": "/stock/stocks/6f2a0542-d65d-489a-b96c-c8658ff195ea",
"#type": "Stock",
"quantityOnHand": 50,
"lastCountedEntryWasCreatedAt": "2020-07-23T10:04:32+02:00",
"warehouse": {
"#id": "/stock/warehouses/cc8f3267-29b6-4ad5-9f8b-74b98aab85d6",
"#type": "Warehouse",
"id": "cc8f3267-29b6-4ad5-9f8b-74b98aab85d6",
"owner": "9002",
"name": "Steuber, Ruecker and Vandervort",
"createdAt": "2020-07-23T10:04:32+02:00"
},
"createdAt": "2020-07-23T10:04:32+02:00"
}
],
"createdAt": "2020-07-23T10:04:32+02:00"
},
How can I filter the subentity result by just using my extension and DQL ? Thanks

Extension is used for retrieve main items, not associated collection.
Instead you can use data transformer.
class ReferenceOutput {
public $stocks;
}
//Next class
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use Symfony\Component\Security\Core\Security;
class ReferenceOutputTransformer implements DataTransformerInterface
{
private string $security;
public function __construct(Security $security)
{
$this->security = $security;
}
/**
* Transforms the given object to something else, usually another object.
* This must return the original object if no transformations have been done.
*
* #param object $object Snapshot
*
* #param string $to
* #param array $context
*
* #return object
*/
public function transform($object, string $to, array $context = [])
{
/** #var Reference $reference */
$reference = $object;
/** #var Client $client */
$client = $this->security->getUser();
$referenceOutput = new ReferenceOutput();
$stocks = $reference->getStoks()->filter(
static function($key, $stock) use ($client) {
return $stock->getWarehouse()->getOwner() === $client->getSubject();
});
$referenceOutput->stocks = $stocks;
return $referenceOutput;
}
/**
* Checks whether the transformation is supported for a given data and context.
*
* #param object|array $data object on normalize / array on denormalize
*
* #param string $to
* #param array $context
*
* #return bool
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return ReferenceOutput::class === $to && $data instanceof Reference;
}
}

Related

Format result for requesting all data

I have my controller with index() and show().
{
/**
* Display a listing of the resource.
*
* #return \App\Http\Resources\MyRessource
*/
public function index()
{
//???
}
/**
* Display the specified resource.
*
* #param \App\Models\Test $test
* #return \App\Http\Resources\TestRessource
*/
public function show(Test $test)
{
return new \App\Http\Resources\TestRessource($test);
}
}
In my resource the show() has the format I want for return, so the result for http://127.0.0.1/Test/1 is the ID 1 with the formatted JSON.
{
"data": {
"id": 1,
"ref": "0103573026466442101007175850",
"tax": null,
"date_in": "2021-10-08T12:37:05.000000Z",
"date_out": "2021-10-11T08:02:17.000000Z"
}
}
I want the index() to return the same way by using my resource.
When I do index() on http://127.0.0.1/Test, it returns all my data but not in the formatted JSON that I want.
Resource code:
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
"id" => $this->id,
"ref" => $this->ref,
"tax" => $this->tax,
"date_in" => $this->date_in,
"date_out" => $this->date_out
];
}
On index() do as in docs.
return TestRessource::collection(Test::all());

Allowing NULL value in json with API-Platform

I have currently this entity and I want to show my property firedDate in my JSON even is the value is null.
/**
* #ApiResource(normalizationContext={"groups"={"employee"}})
* #ApiFilter(DateFilter::class, properties={"dateProperty": DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER})
* #ORM\Table(name="employee")
*/
class Employee
{
// ...
/**
* #ORM\Column(type="datetime", nullable=true)
* #Groups({"employee"})
*/
private $firedDate;
public function getFiredDate(): ?\DateTimeInterface
{
return $this->firedDate;
}
// ...
}
Currently, when the value is null, it's not shown in the response.
I think I found the right solution to this problem.
Set skip_null_values in false in your normalizationContext:
* #ApiResource(
* itemOperations={
* "get" = {
* //...
* }
* "put" = {
* //...
* },
* "patch" = {
* //...
* }
* },
* collectionOperations={
* "get",
* "post" = {
* //...
* }
* },
* normalizationContext={
* "skip_null_values" = false,
* "groups" = {"object:read"}
* },
* denormalizationContext={"groups" = {"object:write"}}
* )
Are you under PHP 7.0 or above?
In PHP 7.1 you can have nullable return types for functions, so your
public function getFiredDate(): ?\DateTime
{
return $this->firedDate;
}
With the ? before \DateTime, the function will return null as well.
On ApiPlatform 3 the default has changed from skip_null_values=false to skip_null_values=true.
If you don't want having to set this on each resource, and would like to have the default as it as on ApiPlatform < 3, you can simply set it on the global config:
api_platform:
defaults:
normalization_context:
skip_null_values: false
Or if you use PHP based configuration:
return static function (Symfony\Config\ApiPlatformConfig $apiConfig): void {
$apiConfig
->defaults()
->normalizationContext(['skip_null_values' => false]);
}
Maybe your entity is missing a getter like this one?
public function getFiredDate(): \DateTime
{
return $this->firedDate;
}
Just get the solution from a friend on github, here is it:
* #ApiResource(
* itemOperations={"get"},
* )
BEFORE:
{
"#context": "/contexts/Employee",
"#id": "/employees/1",
"#type": "Employee",
"id": 1,
"name": "Oliver",
"hired": "2019-10-10T00:00:00+00:00",
"experience": 0,
"salary": "1200.00",
"job": {
"#id": "/employee_jobs/1",
"#type": "EmployeeJob",
"id": 1,
"name": "Mécanicien"
}
}
AFTER:
{
"#context": "/contexts/Employee",
"#id": "/employees/1",
"#type": "Employee",
"id": 1,
"name": "Oliver",
"hired": "2019-10-10T00:00:00+00:00",
"experience": 0,
"salary": "1200.00",
"firedDate": null,
"job": {
"#id": "/employee_jobs/1",
"#type": "EmployeeJob",
"id": 1,
"name": "Mécanicien"
}
}

Logic error php / Symfony

Hey guys I have a little problem I will try to explain you (is not a technic problem just logic) :
So I have an mongoDB database and I send and receive message (I am doing a message system from user to user) and when I try to see the message and I am doing this for an API.
The problem is that when I do the post method I have to pass as parameter this :
{
"id_transmitter": 2,
"token": "eyJ0...",
"id_receiver": 4
}
Response :
{
"success": "true",
"message": [
[
{
"id": "5b71861fe138231d423a95ef",
"content": "5",
"transmitter": 2,
"Receiver": 4,
"send_at": {
"sec": 1534166559,
"inc": 1
}
}
],
[
{
"id": "5b71861ce138231d423a95ee",
"content": "4",
"transmitter": 2,
"Receiver": 4,
"send_at": {
"sec": 1534166556,
"inc": 1
}
}
],
[
{
"id": "5b718619e138231d423a95ed",
"content": "3",
"transmitter": 2,
"Receiver": 4,
"send_at": {
"sec": 1534166553,
"inc": 1
}
}
],
[
{
"id": "5b718615e138231d423a95ec",
"content": "2",
"transmitter": 2,
"Receiver": 4,
"send_at": {
"sec": 1534166549,
"inc": 1
}
}
]
],
"more": 1
}
As you can see I pass the receiver and the message to see and here is the problem I show only the message that we received OR that we send but not both at same time ... or I have to do 2 repository one with all received and one other with sended and when I will try to show by order of date that will print first one repo and after the other ... and not all together like a converstion should be ...
Here you can find how i did :
/**
*
* #Rest\Post(
* path = "/message/user/list/{page}",
* name = "api_message_list"
* )
*/
public function UserSeeMessageAction(Request $request, $page)
{
$dm = $this->get('doctrine_mongodb');
$messages = $dm->getRepository('AppBundle:MessageUser')->findBy(array
(
'idTransmitter' => ($request->get('id_transmitter')),
'idReceiver' => ($request->get('id_receiver')),
), array('id' => 'DESC'));
//return new JsonResponse([
// 'success' => "true",
// 'message' => $messages,
// 'more' => 0,
//]);
if (($request->get('id_receiver')) == null) {
return new JsonResponse([
'success' => "false",
'message' => "Receveur non renseigné"
]);
}
if (($request->get('id_transmitter')) == null) {
return new JsonResponse([
'success' => "false",
'message' => "Transmetteur non renseigné"
]);
}
if (!isset($messages)) {
return new JsonResponse([
'success' => "false",
'message' => "Pas de messages pour les utilisateurs"
]);
}
$arrayCollection = array();
$entities = 4;
$y = 0;
$x = 0;
foreach($messages as $message) {
if ($y >= ($page * $entities)) {
return new JsonResponse([
'success' => "true",
'message' => $arrayCollection,
'more' => 1,
]);
}
if ($y >= (($page - 1) * $entities) and $y < (($page) * $entities)) {
$x = 1;
$arrayCollection[] = array(
$message,
);
}
$y += 1;
}
if ($x == 1) {
return new JsonResponse([
'success' => "true",
'message' => $arrayCollection,
'more' => 0,
]);
}
return new JsonResponse([
'success' => "false",
'message' => "Plus de message",
]);
}
So for resume : my problem is that I print only sended message or received but not all together
Exemple :
Like it should be :
Hello i am one (send by one)
Hello one ! I am two (send by two)
How are you (send by one)
Fine (send by two)
Like i have :
Hello i am one (send by one)
How are you (send by one)
Hello one ! I am two (send by two)
Fine (send by two)
Like my problem is that i don't know how to set two repo in one .. i want a repo with the messages received and sended
Thx for all that will try to help ! I hope that you understand my bad explanation :)
Nb : The entity :
<?php
namespace AppBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document()
*/
class MessageUser implements \JsonSerializable
{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\Field(type="string")
*/
protected $content;
/**
* #MongoDB\Field(type="timestamp")
*/
protected $sendAt;
/**
* #MongoDB\Field(type="boolean")
*/
protected $read;
/**
* #MongoDB\Field(type="integer")
*/
protected $idTransmitter;
/**
* #MongoDB\Field(type="integer")
*/
protected $idReceiver;
/**
* Get id
*
* #return $id
*/
public function getId()
{
return $this->id;
}
/**
* Set content
*
* #param string $content
* #return $this
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* #return string $content
*/
public function getContent()
{
return $this->content;
}
/**
* Set sendAt
*
* #param $sendAt
* #return $this
*/
public function setSendAt($sendAt)
{
$this->sendAt = $sendAt;
return $this;
}
/**
* Get sendAt
*
* #return $sendAt
*/
public function getSendAt()
{
return $this->sendAt;
}
/**
* Set read
*
* #param boolean $read
* #return $this
*/
public function setRead($read)
{
$this->read = $read;
return $this;
}
/**
* Get read
*
* #return boolean $read
*/
public function getRead()
{
return $this->read;
}
/**
* Set idTransmitter
*
* #param integer $idTransmitter
* #return $this
*/
public function setIdTransmitter($idTransmitter)
{
$this->idTransmitter = $idTransmitter;
return $this;
}
/**
* Get idTransmitter
*
* #return integer $idTransmitter
*/
public function getIdTransmitter()
{
return $this->idTransmitter;
}
/**
* Set idReceiver
*
* #param integer $idReceiver
* #return $this
*/
public function setIdReceiver($idReceiver)
{
$this->idReceiver = $idReceiver;
return $this;
}
/**
* Get idReceiver
*
* #return integer $idReceiver
*/
public function getIdReceiver()
{
return $this->idReceiver;
}
function jsonSerialize()
{
return array(
"id" => $this->id,
"content" => $this->content,
"transmitter" => $this->idTransmitter,
"Receiver" => $this->idReceiver,
"send_at" => $this->sendAt,
);
}
}

Exclude null properties in JMS Serializer

My consumed XML API has an option to retrieve only parts of the response.
This causes the resulting object to have a lot of NULL properties if this feature is used.
Is there a way to actually skip NULL properties? I tried to implement an exclusion strategy with
shouldSkipProperty(PropertyMetadata $property, Context $context)`
but i realized there is no way to access the current property value.
An example would be the following class
class Hotel {
/**
* #Type("string")
*/
public $id;
/**
* #Type("integer")
*/
public $bookable;
/**
* #Type("string")
*/
public $name;
/**
* #Type("integer")
*/
public $type;
/**
* #Type("double")
*/
public $stars;
/**
* #Type("MssPhp\Schema\Response\Address")
*/
public $address;
/**
* #Type("integer")
*/
public $themes;
/**
* #Type("integer")
*/
public $features;
/**
* #Type("MssPhp\Schema\Response\Location")
*/
public $location;
/**
* #Type("MssPhp\Schema\Response\Pos")
*/
public $pos;
/**
* #Type("integer")
*/
public $price_engine;
/**
* #Type("string")
*/
public $language;
/**
* #Type("integer")
*/
public $price_from;
}
which deserializes in this specific api call to the following object with a lot of null properties.
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"address": null,
"themes": null,
"features": null,
"location": null,
"pos": null,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
But i want it to be
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
You can configure JMS Serializer to skip null properties like so:
$serializer = JMS\SerializerBuilder::create();
$serializedString = $serializer->serialize(
$data,
'xml',
JMS\SerializationContext::create()->setSerializeNull(true)
);
Taken from this issue.
UPDATE:
Unfortunately, if you don't want the empty properties when deserializing, there is no other way then removing them yourself.
However, I'm not sure what your use case for actually wanting to remove these properties is, but it doesn't look like the Hotel class contains much logic. In this case, I'm wondering whether the result has should be a class at all ?
I think it would be more natural to have the data represented as an associative array instead of an object. Of course, JMS Serializer cannot deserialize your data into an array, so you will need a data transfer object.
It's enough that you add dumpArray and loadArray methods to your existing Hotel class. These will be used for transforming the data into your desired result and vice versa. There is your DTO.
/**
* Sets the object's properties based on the passed array
*/
public function loadArray(array $data)
{
}
/**
* Returns an associative array based on the objects properties
*/
public function dumpArray()
{
// filter out the properties that are empty here
}
I believe it's the cleanest approach and it might reflect what you're trying to do more.
I hope this helps.

How to configure the output of an object during json_encode?

A price object has three properties,
/** #var float */
public $amount = 0.0;
/** #var string */
public $currency = '';
/**
* #var \DateTime
*/
public $dueDate;
When serializing this object to json via the symfony2 Symfony\Component\HttpFoundation\JsonResponse, it will look like:
{
"price": {
"amount": 235,
"currency": "EUR",
"dueDate": {
"date": "2015-10-25 00:00:00.000000",
"timezone": "UTC",
"timezone_type": 3
}
}
}
I want the \DateTime to be formatted as simply a string:
"dueDate": "2015-10-22 00:00:00.000000"
How to get this done is not the scope of the question, as I currently handle this case in the object's constructor:
/**
* Price constructor.
* #param float $amount
* #param string $currency
* #param \DateTime|null $dueDate
*/
public function __construct($amount = 0.0, $currency = "", $dueDate)
{
$this->amount = $amount;
$this->currency = $currency;
$this->dueDate = $dueDate;;
if ($dueDate instanceof \DateTime) {
$this->dueDate = $dueDate->format(\DateTime::ATOM);
}
}
yet it doesn't feel entirely right, and I am curious if I could configure the serialize process differently, in the sense, instead of coding my representation, modify the way the object is serialized.
Reasoning is to have all \DateTime objects serialized that are serialized wherever in an to-be-serialized object in a same specific way, without duplicating logic. (I guess I could put the handling in an abstract class or somewhere similar, yet extending objects also has its pitfalls)
Basically:
Is there a catch an onserialize "event" where I can add some logic, or am I better off looking into JMSSerializer?
I don't know why I didn't submit this as the answer. Since PHP 5.4.0 a JsonSerializable library class is shipped with the PHP install. You can implement this class on your own and create a method named jsonSerialize that will be called whenever json_encode() is called with the class as the argument. A solution to your predicament could be similar to this:
<?php
class Price implements JsonSerializable {
private
$amount
, $currency
, $dueDate
;
/**
* Price constructor.
* #param float $amount
* #param string $currency
* #param \DateTime|null $dueDate
*/
public function __construct($amount = 0.0, $currency = "", $dueDate = NULL)
{
$this->amount = $amount;
$this->currency = $currency;
$this->dueDate = $dueDate;
}
public function jsonSerialize(){
return array(
'amount' => $this->amount
, 'currency' => $this->currency
, 'dueDate' => $this->dueDate instanceof \DateTime ? $this->dueDate->format(\DateTime::ATOM) : $this->dueDate
}
}
echo json_encode(new Price(235, "EUR", new DateTime()));
So you have 3 options:
Use the JsonSerializable interface like #iam-coder proposed.
Use a full-blown serializer like JMS (can be slow).
Use a transformer, the plus side of this is that your output is decoupled from your data, and you can change and test each component on it's own.

Categories