Hi Restler/Swagger friends,
I have a problem when trying to submit values of object that have inner object values to service side..
what happened that object values submitted but inner object values didn't submitted to the service side or submitted but not processed at service side.....
How can i submit the inner object values or allow service side to take theses inner objects?
Do i need to update php at service side?
if yes where should i update?
please guide me
My test classes as follow:
/**
* POST
* post Author
*
* #param {#type Author} $a
*
* #return Author
*/
function postAuthor(Author $a) {
return $a->p->fname." ".$a->name;
}
//models
//inner object
class Person {
/**
* #var string {#from body}
* name of the Person {#required true}
*/
public $fname = 'First name';
/**
* #var string {#from body}
* last name of the Person {#required true}
*/
public $lname = 'Last name';
/**
* #var array $arr {#type array} array of IDs
* {#required true}
*/
public $arr = array();
}
//outer object
class Author {
/** define person as inner object
* #var Person $p {#type Person} {#from body} person of author
*
*/
public $p ;
/**
* #var string {#from body}
* name of the Author {#required true}
*/
public $name = 'Name';
/**
* #var string {#type email} {#from body}
* email id of the Author
*/
public $email = 'name#domain.com';
}
when i fill the values using json default value as follow:
{
"p":{
"fname": "aa",
"lname": "bb",
},
"name":"aa",
"email":"aa#hotmail.com"
}
then click Try it prints
{
"p": {
"fname": "First name",
"lname": "Last name",
"arr": []
},
"name": "aa",
"email": "aa#hotmail.com"
}
this means that json is submitted but inner object values not processed and not returned to test side.
We just released an update in V3 branch that fixes this. Check it out
Related
I have the structure like below.
----------------
MESSAGE
----------------
id
subject
body
----------------
----------------
USER
----------------
id
name
category
region
----------------
----------------
RECIPIENT
----------------
user_id
message_id
is_read
read_at
----------------
So Message 1:n Recipient m:1 User.
Recipient is not an #ApiResource.
A Backoffice user will "write" a message and choose the audience by a set of specific criteria (user region, user category, user tags...).
To POST the message i'm using a Dto
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public DateTimeInterface $publishDate;
/**
* #var DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
*/
public array $criteria = [];
}
The $criteria field is used to choose the audience of that message and is skipped by the DataTransformer as it is not a mapped field, a property of Message Entity that is returned by the transformer.
class MessageInputDataTransformer implements \ApiPlatform\Core\DataTransformer\DataTransformerInterface
{
/**
* #var MessageInputDto $object
* #inheritDoc
*/
public function transform($object, string $to, array $context = [])
{
$message = new Message($object->subject, $object->body);
$message->setIsPublished($object->isPublished);
$message->setPublishDate($object->publishDate);
$message->setExpiryDate($object->expiryDate);
$message->setCategory($object->category);
return $message;
}
/**
* #inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a book we transformed the data already
if ($data instanceof Message) {
return false;
}
return Message::class === $to && null !== ($context['input']['class'] ?? null);
}
}
As side effect, will be performed a bulk insert in the join table (Recipient) that keeps the m:n relations between Message and User.
My problem is how/where to perform this bulk insert and how pass the $criteria to the service that will manage it.
The only solution that i've found now (and it's working but i don't think is a good practice) is to put the bulk insert procedure in the POST_WRITE event of the Message, get the Request object and process the $criteria contained there.
class MessageSubscriber implements EventSubscriberInterface
{
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['handleCriteria', EventPriorities::POST_WRITE]
],
];
}
public function handleCriteria(ViewEvent $event)
{
/** #var Message $message */
$message = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$e = $event->getRequest();
$collectionOperation = $e->get('_api_collection_operation_name');
if (!$message instanceof Message ||
$method !== Request::METHOD_POST ||
$collectionOperation !== 'post') {
return;
}
$content = json_decode($event->getRequest()->getContent(), true);
if(array_key_exists('audienceCriteria', $content)){
$criteria = Criteria::createFromArray($content['audienceCriteria']);
// Todo: Create the audience
}
}
}
So the idea is that, when the Message is persisted, the system must generate the "relations" public.
This is why i think that the post write event could be a good choice, but as i said i'm not sure this could be a good practice.
Any idea? Thanks.
As the docs on DTO's state: "in most cases the DTO pattern should be implemented using an API Resource class representing the public data model exposed through the API and a custom data provider. In such cases, the class marked with #ApiResource will act as a DTO."
IOW specifying an Input or an Output Data Representation and a DataTransformer is the exception. It does not work if the DTO holds more data then the entity or if the dto's are not one to one with the entities (for example with a report that does a group by).
Here is your DTO class as a resource:
namespace App\DTO;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Entity\Message;
/**
* Class defining Message data transfer
*
* #ApiResource(
* denormalizationContext= {"groups" = {"msg_message:write"}},
* itemOperations={
* },
* collectionOperations={
* "post"={
* "path"="/messages",
* "openapi_context" = {
* "summary" = "Creates a Message",
* "description" = "Creates a Message"
* }
* }
* },
* output=Message::class
* )
*/
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var \DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public \DateTimeInterface $publishDate;
/**
* #var \DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?\DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
* #Groups({"msg_message:write"})
*/
public array $criteria = [];
}
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
If MessageInputDto is in /src/DTO make it like:
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/src/DTO'
The post operation may have the same path as dhe default post operation on your Message resource. Remove that by explicitly defining collectionOperations for your Message resource without "post".
The post operation of MessageInputDto will deserialize the MessageInputDto. Your DataTransformer will not act on it so that it will arrive as is to the DataPersister:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\MessageInputDto;
use App\Entity\Message;
use Doctrine\Persistence\ManagerRegistry;
use App\DataTransformer\MessageInputDataTransformer;
use ApiPlatform\Core\Exception\InvalidArgumentException;
class MessageDataPersister implements ContextAwareDataPersisterInterface
{
private $dataPersister;
private $entityManager;
private $dataTransformer;
public function __construct(ContextAwareDataPersisterInterface $dataPersister, ManagerRegistry $managerRegistry, MessageInputDataTransformer $dataTransformer)
{
$this->dataPersister = $dataPersister;
$this->entityManager = $managerRegistry->getManagerForClass(Message::class);
$this->dataTransformer = $dataTransformer;
}
public function supports($data, array $context = []): bool
{
$transformationContext = ['input' => ['class' => Message::class]];
return get_class($data) == MessageInputDto::class
&& $this->dataTransformer->supportsTransformation($data, Message::class, $transformationContext)
&& null !== $this->entityManager;
}
public function persist($data, array $context = [])
{
$message = $this->dataTransformer->transform($data, Message::class);
// dataPersister will flush the entityManager but we do not want incomplete data inserted
$this->entityManager->beginTransaction();
$commit = true;
$result = $this->dataPersister->persist($message, []);
if(!empty($data->criteria)){
$criteria = Criteria::createFromArray($data->criteria);
try {
// Todo: Create the audience, preferably with a single INSERT query SELECTing FROM user_table WHERE meeting the criteria
// (Or maybe better postpone until message is really sent, user region, category, tags may change over time)
} catch (\Exception $e) {
$commit = false;
$this->entityManager->rollback();
}
}
if ($commit) {
$this->entityManager->commit();
}
return $result;
}
public function remove($data, array $context = [])
{
throw new InvalidArgumentException('Operation not supported: delete');
}
}
(Maybe it should have been called MessageInputDtoDataPersister - depending on how you look at it)
Even with service autowiring and autoconfiguration enabled, you must still configure it to get the right dataPersister to delegate to:
# api/config/services.yaml
services:
# ...
'App\DataPersister\MessageDataPersister':
arguments:
$dataPersister: '#api_platform.doctrine.orm.data_persister'
This way you do not need MessageSubscriber.
Be aware that all the other phases inbetween deserialization and data persist (validation, security post denormalize) work on the MessageInputDto.
One solution when you have to generate multiple custom entities is to use data persisters: https://api-platform.com/docs/core/data-persisters/
There you have 2 options:
Decorate the doctrine persister - meaning the message will still be saved by Doctrine, but you can do something before or afterwards.
Implement a custom persister - saving both message and other related entities that you like. Or doing something completely custom, without calling Doctrine at all.
I have try to create a Custom REST POST plugin in my Drupal 8.3.2 for get an external JSON and then create an article from that.
I have follow that guide: How to create Custom Rest Resources for POST methods in Drupal 8
And this is my code:
<?php
namespace Drupal\import_json_test\Plugin\rest\resource;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\node\Entity\Node;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Psr\Log\LoggerInterface;
/**
* Provides a resource to get view modes by entity and bundle.
*
* #RestResource(
* id = "tio_rest_json_source",
* label = #Translation("Tio rest json source"),
* serialization_class = "Drupal\node\Entity\Node",
* uri_paths = {
* "canonical" = "/api/custom/",
* "https://www.drupal.org/link-relations/create" = "/api/custom"
* }
* )
*/
class TioRestJsonSource extends ResourceBase {
/**
* A current user instance.
*
* #var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs a new TioRestJsonSource object.
*
* #param array $configuration
* A configuration array containing information about the plugin
instance.
* #param string $plugin_id
* The plugin_id for the plugin instance.
* #param mixed $plugin_definition
* The plugin implementation definition.
* #param array $serializer_formats
* The available serialization formats.
* #param \Psr\Log\LoggerInterface $logger
* A logger instance.
* #param \Drupal\Core\Session\AccountProxyInterface $current_user
* A current user instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger,
AccountProxyInterface $current_user) {
parent::__construct($configuration, $plugin_id,
$plugin_definition, $serializer_formats, $logger);
$this->currentUser = $current_user;
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container, array
$configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('import_json_test'),
$container->get('current_user')
);
}
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
* #param $node_type
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($node_type, $data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
$node = Node::create(
array(
'type' => $node_type,
'title' => $data->title->value,
'body' => [
'summary' => '',
'value' => $data->body->value,
'format' => 'full_html',
],
)
);
$node->save();
return new ResourceResponse($node);
}
}
Now if i try to test this without passing a payload and modifing the return value in this way:
return new ResourceResponse(array('test'=>'OK'));
It's working!
But if i send a custom payload like this using my custom code above:
{
"title": [{
"value": "Test Article custom rest"
}],
"type": [{
"target_id": "article"
}],
"body": [{"value": "article test custom"}]
}
I recieve a 400 Error with: Symfony\Component\HttpKernel\Exception\BadRequestHttpException: The type link relation must be specified. in Drupal\rest\RequestHandler->handle() (line 103 of core/modules/rest/src/RequestHandler.php).
What's going Wrong?
Thx.
I have find a solution:
I have removed the annotation:
* serialization_class = "Drupal\node\Entity\Node",
Then i take care just for data in my post function:
/**
* Responds to POST requests.
*
* Returns a list of bundles for specified entity.
*
* #param $data
*
*
* #return \Drupal\rest\ResourceResponse
*
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($data) {
// You must to implement the logic of your REST Resource here.
// Use current user after pass authentication to validate access.
if (!$this->currentUser->hasPermission('access content')) {
throw new AccessDeniedHttpException();
}
return new ResourceResponse(var_dump($data));
The important thing is, when you use postman for example, is to add an header with Content-Type -> application/json:
Instead of Content-Type -> application/hal+json
With this configuration i can post any type of JSON and then manage it as i prefer.
Bye!
Basically I create a form that uses multiple entity, and the result retrieved from this form I wish the stock in another table separated into BDD. I was told to make an insertion (or update) request in a repository and call it from the controller after checking the submitted data. But in this case there will be no persist or flush in this case since you do not save an object corresponding to an entity there will be no persist or flush something like that, but I arrive Not really have to do it.
That's why I want to store in another table:
When I validate my form, my result retrieved by my form is stocked here (id: 6)
this is my code :
public function testAction(Request $request ){
$poste = new Poste();
$formPoste = $this->get('form.factory')->create(PosteType::class, $poste );
$historique = new Historique_employer();
if ($request->isMethod('POST')&& $formPoste->handleRequest($request)->isValid()) {
$historique->setPoste($request->request['poste_id']->getData());
$historique->setCity($request->request['City']->getData());
$historique->setEmployer($request->request['Employer']->getData());
$historique->setTemps($request->request['Temps']->getData());
dump($poste);die();
$em = $this->getDoctrine()->getManager();
$em->persist($poste);
$em->persist($historique);
$em->flush();
}
return $this->render(':stat:teste.html.twig', array(
'poste' => $formPoste->createView(),
'historique' => $historique,
));
}
and I would like store my data in this entity :
class Historique_employer
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Poste
* #ORM\OneToOne(targetEntity="Poste")
*/
private $poste;
/**
* #var City
* #ORM\OneToOne(targetEntity="City")
*/
private $city;
/**
* #var Temps
* #ORM\OneToOne(targetEntity="Temps")
*/
private $temps;
/**
* #var Employer
* #ORM\OneToOne(targetEntity="Employer")
*/
private $employer;
but when i do all that I have this error message :
Cannot use object of type Symfony\Component\HttpFoundation\ParameterBag as array
Symfony 2.8 ParameterBag
you are accessing the request parameters like arrays use
mixed get(string $key, mixed $default = null, bool $deep = false)
$historique->setPoste($request->request->get('poste_id'));
change the rest and you are good go.
Let's say I've got an entity called User with some properties like firstName, lastName, age and collection of Posts.
Is there a way to load this User entity with the EntityManager and Find-method without loading all properties/collections?
I'm using a REST API to retrieve the User and I want to seperate the calls:
api/user/1 - should retrieve the User object with only firstName, lastName and age.
api/user/1/posts - should retrieve the User object with the properties and a collection of Posts.
I know I can use QueryBuilder and create seperate methods to retrieve the desired data, but I want to have it this way:
The scenario:
api/user/1
$userId = 1;
$user = getEntityManager()->find('entity\User', $userId);
return $user; // should only load the firstName, lastName, age properties.
api/user/1/posts
$userId = 1;
$user = getEntityManager()->find('entity\User', $userId);
return $user->getPosts(); // should load the firstName, lastName, age properties and the collection of posts.
I already tried 'Extra Lazy Load' functionality that is built-in Doctrine, without any result.
My code:
<?php
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name = "USER")
*
* #JMS\ExclusionPolicy("all")
* #JMS\AccessorOrder("custom", custom = { "id", "firstName", "lastName" })
*/
class User {
/**
* #ORM\Id
* #ORM\Column(name = "id", type = "integer", options={"unsigned"=true})
* #ORM\GeneratedValue(strategy = "IDENTITY")
* #JMS\SerializedName("id")
* #JMS\Expose
* #JMS\Type("integer")
* #var int
*/
private $id;
/**
* #ORM\Column(name = "firstName", type = "string", length = 255)
* #JMS\SerializedName("firstName")
* #JMS\Expose
* #JMS\Type("string")
* #var string
*/
private $firstName;
/**
* #ORM\Column(name = "lastName", type = "string", length = 255)
* #JMS\SerializedName("lastName")
* #JMS\Expose
* #JMS\Type("string")
* #var string
*/
private $lastName;
/**
* #ORM\OneToMany(targetEntity = "Post", mappedBy = "user", cascade = { "persist" }, fetch="LAZY")
* #JMS\Expose
* #var ArrayCollection<Post>
*/
private $posts;
public function getId() {
return $this->id;
}
public function getFirstName() {
return $this->firstName;
}
public function getLastName() {
return $this->lastName;
public function getPosts() {
return $this->posts->toArray();
}
}
The posts are always loaded after fetching the user object, even though I use fetch type lazy or extra_lazy.
Why not using Serialization Groups for it?
You can annotate your group (e.g. {'default'} and {'collection'} or even more specific {'posts'}.
Then you simply set your Serialization Context in your Controller and only the Group-related properties will be serialized.
See as well documentation
You should write proper dql/querybuilder query and select only required for You fields.
It's not safe to return all posts on API just with $user->getPosts() - you will need pagination here.
You can also use JMSSerializerBundle. It gives you option to define needed property for Entity.
Hi Swagger/Restler friends,
How can I allow users to make easy test for php function and classes?
I have a class as follow:
class Author
{
/**
* #var string {#from body} {#min 3}{#max 100}
* name of the Author {#required true}
*/
public $name = 'Name';
/**
* #var string {#type email} {#from body}
* email id of the Author
*/
public $email = 'name#domain.com';
}
and I want to generate html documentation for a class that is as the follow:
class ComplexType {
/**
* post 2 Authors
*
* #param Author $author1
* #param Author $author2
*
* #return Author
*/
function post2Authors(Author $author1,Author $author2) {
return $author1;
}
}
It gives me when I run index.html the following to input:
{
"author1": "",
"author2": ""
}
But I need to view json input as follow:
{
"author1":
{
"name": "",
"email": ""
},
"author2": {
"name": "",
"email": ""
}
}
thank you in advance
Default value serves as an easy starter for trying the API with Restler API Explorer.
Currently it does not offer model parsing when more than one body parameter is found thus we are stuck with
{
"author1": "",
"author2": ""
}
We are working on support for Swagger 1.2 spec and along with that we will be releasing the full model parsing for default value along with that soon