mapado / rest-client-sdk Goto Github PK
View Code? Open in Web Editor NEWRest Client SDK for hydra API
License: MIT License
Rest Client SDK for hydra API
License: MIT License
Hi there,
We have an entity hierarchy as such (simplified) :
We have an API end-point for documents which allows to make queries based on shared properties (id, title, author) from the Document base class. Here is a sample response for GET /documents :
{
"@context": "/contexts/Document",
"@id": "/documents",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/articles/320d346d-0c8f-11e7-8f02-0242ac150002",
"@type": "Article",
"id": "320d346d-0c8f-11e7-8f02-0242ac150002",
"title": "I am an article",
"author": "Me",
"content": "article content"
},
{
"@id": "/recipes/320d45f8-0c8f-11e7-8f02-0242ac150002",
"@type": "Recipe",
"id": "320d45f8-0c8f-11e7-8f02-0242ac150002",
"title": "I am a recipe",
"author": "You",
"ingredients": "food and stuff",
"calories": 5000
}
],
"hydra:totalItems": 2,
"hydra:search": {
// ...
}
}
And then, when we try to call this end-point with the SDK client, the model hydrator explodes:
Cannot instantiate abstract class AppBundle\Entity\Document
Stack Trace
in vendor/mapado/rest-client-sdk/src/Model/Serializer.php at line 84 -
$identifierAttribute = $classMetadata->getIdentifierAttribute();
$identifierAttrKey = $identifierAttribute ? $identifierAttribute->getSerializedKey() : null;
$instance = new $className();
foreach ($data as $key => $value) {
$attribute = $classMetadata->getAttribute($key);
}
Of course, the Document is not to be instantiated as it is abstract. The model hydrator receives the "entityName" from an entity repository (DocumentRepository in our case), and then asks to hydrate the list of entities with this entityName, but in fact it should use the "@type" of the collection items to instantiate each entity, and maybe just use the entity name to validate the instantiated entities?
What do you think, is there a way to make it work?
When the serializer deserialize the entity with:
$agente = $this->get('mapado.rest_client_sdk.agentes')->getRepository('agentes')->find(20);
if that entity, in this case "Agentes", have a relation with other entity but that entity doesn't have an iri identifier the app throws an exception like this:
That error means that the entity is handled like an resourse but it isn't.
But maybe something like other annotation or maybe an if condition on the desiarialization can make that a non resourse entity relation be handled as a rest relation.
i made something like that with the controller scope:
$serializer = $this->get('mapado.rest_client_sdk.agentes')->getSerializer();
$agente = $this->get('mapado.rest_client_sdk.agentes')->getRepository('agentes')->find(20);
$agente->setTipoCuil($serializer->deserialize($agente->getTipoCuil(), 'App\Entity\TipoCuil'));
This only works if you put the rest\entity annotation on the relation entity:
/**
* TipoCuil
* @Rest\Entity(key="tipoCuil")
*/
class TipoCuil`
and in Agentes:
`/**
* @var array
*
* @Rest\Attribute(name="tipoCuil", type="array")
*/
private $tipoCuil;
this would make all relationships be objects and no array like now when the relation is an nono iri relation.
sorry for my bad english and for the latest issues. This is a really good and useful project and i want to help with everything ahah
Use property accessor to replace get{$method}
and set{$method}
First of all let me define my resouces.
I use this Hydra API: http://www.markus-lanthaler.com/hydra/event-api/events/41
I created a class Event
like:
/**
* Class Event
*
* @package Acme\DemoBundle\Entity
* @Rest\Entity(key="events", client="Acme\DemoBundle\Command\BasicClient")
*/
class Event
{
/**
* @var int
* @Rest\Id()
*/
protected $id;
/**
* @var string
* @Rest\Attribute(name="name", type="string")
*/
protected $firstName;
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* @return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* @param string $firstName
*/
public function setFirstName($firstName)
{
$this->firstName = $firstName;
}
}
Now when I try to get the Event Entity the Id is correct. But the $firstName
attribute gives me back NULL
.
When I change the $firstName
attribute to $name
, everything works like a charm.
It looks like the @Attribute name annotation doesn't seem to work as it should.
Hello,
I have a problem handling 400 response from API.
My problem is that I need to show a particular message when an admin tries to create a new user with an already used email.
The API returns this when it happens:
Response status code: 400
Response body: - Toggle response
{
"@context": "\/contexts\/ConstraintViolationList",
"@type": "ConstraintViolationList",
"hydra:title": "An error occurred",
"hydra:description": "email: This value is already used.",
"violations": [
{
"propertyPath": "email",
"message": "This value is already used."
}
]
}
In my controller however, if I Try/Catch the call to the repository, I only have access to the ClientException (thrown in Mapado\RestClientSdk\RestClient::post), so I can't distinguish the particular "dupplicate email" error from other ones, even if the API sent the information in it's 400 response.
Please help, Merci beaucoup
This follows #95, it could be usefull to change datetime timezone before formating.
This operation should not alter original value, so we need to convert DateTime
instances to DateTimeImmutable
(with DateTimeImmutable::fromMutable($d)
) to do the job.
Not sure if this is an issue on the SDK side or the API side.
I'm querying an API endpoint which should return a collection of items (CGET), with a page (1) and a number of items per page (10), and I would expect to access the real total item count (5000), to build a pagination system. This information seem to be available through the HydraPaginatedCollection class and getTotalItems() method, but I always get a HydraCollection object, which only shows the number of returned items (10).
I'm making an API call with these parameters:
GET domain.tld/items?foo=bar&order[name]=ASC&itemsPerPage=10&page=1
And here is a sample of the response :
{
"@context": "\/contexts\/Item",
"@id": "\/items",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "\/items\/bd87b2df-000b-11e7-a37a-0242ac150003",
"@type": "Item",
"id": "bd87b2df-000b-11e7-a37a-0242ac150003",
"name": "foo"
},
( more items here )
],
"hydra:totalItems": 5000,
"hydra:view": {
"@id": "\/items?foo=bar&order%5Bname%5D=ASC&itemsPerPage=10&page=1",
"@type": "hydra:PartialCollectionView",
"hydra:first": "\/items?foo=bar&order%5Bname%5D=ASC&itemsPerPage=10&page=1",
"hydra:last": "\/items?foo=bar&order%5Bname%5D=ASC&itemsPerPage=10&page=500",
"hydra:next": "\/items?foo=bar&order%5Bname%5D=ASC&itemsPerPage=10&page=2"
},
"hydra:search": {
( ... irrelevant stuff here ... )
}
}
The ModelHydrator::deserializeAll() method seems to rely on the value of the @type
node to build a HydraPaginatedCollection instead of a HydraCollection :
$hydratedList = new HydraCollection($data);
if (!empty($data['@type'])) {
if ($data['@type'] === 'hydra:PagedCollection') {
$hydratedList = new HydraPaginatedCollection($data);
}
}
return $hydratedList;
But in my response, the value of @type
is 'hydra:Collection'.
Do you think the condition is too strict, or the API response too "loose" ? (API is driven by api-platform).
We do accept that two fields contains @Rest\Id
, we should throw an error on that as the id is the url we will call for update / delete and only one is possible
As mentionned in #12 (comment) , it would be nice to try another http client as guzzle may be really conflicting (most used http client, lots of major versions).
Today, the ID needs to be an IRI with the full path.
It works fine with JSON-LD API but does not on other cases.
See #46 for more details
Hi,
we are using the SDK (with the Symfony bundle) with a REST API which has no URL prefix, ie, we have end-points like these:
http://domain.tld/articles
http://domain.tld/articles/434856bf-e37e-11e6-b8b0-0242ac110002
In the YML configs, we put these values:
server_url: 'http://domain.tld'
mappings:
prefix: ~
dir: '%kernel.root_dir%/../src/AppBundle/Entity/'
On our entities:
/**
* @Rest\Entity(key="articles")
*/
class Article
{
[...]
}
But calls made by the SDK look like this (notice the missing slash /):
http://domain.tldarticles/434856bf-e37e-11e6-b8b0-0242ac110002
We then changed the YML property "prefix" from NULL (~) to '/' but then we get this:
http://domain.tld//articles/434856bf-e37e-11e6-b8b0-0242ac110002
We sort of hacked by putting 'http://domain.tld//' (2 slashes) in the server_url and a NULL prefix, but we get many other problems after (entities are converted to identifiers as "articles/...." instead of "/articles/...." for example).
Is the absence of URL prefix supported? If so, how should we configure the SDK?
Thanks in advance.
new hydra should be based on hyra:view object instead of @type :
rest-client-sdk/src/Model/ModelHydrator.php
Line 158 in 680e765
Wouldn't it be easier to add a default Client to the @Entity
annotation if none is given.
Just like Doctrine does with the EntityRepository
?
Wouldn't it be nicer to have a default Client which handles the Conversion and persisting, and a separate class like the Doctrine EntityRepository
to handle lookups?
If we want to load a specific filter, we can create an EntityRepository and add the specific methods to load something specific. Next add the EntityRepository to the Entity.
If we would want to persist or convert in a different way, we extend the Client and add it to the Entity tell the sdkClient to use a different client
When we try to update entity with relationships, sdk does not serialize relationships properly. In this example walletList is serialized as an array of strings (proper way) although minisiteList is serialized as an array of Minisite objects (thus not serialized?).
In this example, the contract we get has ids for wallets, and json objects for minisites, but if we get ids for minisites too the problem remains the same.
/**
* Contract Model
*
* @Rest\Entity(key="contracts", repository="Mapado\Component\Ticketing\Model\Repository\ContractRepository")
*/
class Contract extends HasShortId
{
/**
* @var integer
* @Rest\Attribute(name="id", type="string")
* @Groups({"ticketing"})
*/
private $id;
/**
* walletList
*
* @var array
* @access private
*
* @Rest\OneToMany(name="walletList", targetEntity="Wallet")
* @Groups({"wallet"})
*/
private $walletList;
/**
* minisiteList
*
* @var array
* @access private
*
* @Rest\OneToMany(name="minisiteList", targetEntity="Minsite")
*/
private $minisiteList;
}
/**
* Minisite Model
*
* @Rest\Entity(key="minisites", repository="Mapado\Component\Ticketing\Model\Repository\MinisiteRepository")
*/
class Minisite extends HasShortId
{
/**
* id
*
* @Rest\Id
* @Rest\Attribute(name="id", type="string")
* @Groups({"contract", "ticketing"})
*
* @var string
* @access private
*/
private $id;
/**
* contract
*
* @var Contract
* @access private
*
* @Rest\ManyToOne(name="contract", targetEntity="Contract")
* @Groups({"ticketing"})
*/
private $contract;
}
/**
* Wallet Model
*
* @Rest\Entity(key="wallets")
*
*/
class Wallet extends HasShortId
{
/**
* @var integer
*
* @Rest\Id
* @Rest\Attribute(name="id", type="string")
*/
private $id;
/**
* contract
*
* @var Contract
* @access private
*
* @Rest\ManyToOne(name="contract", targetEntity="Contract")
* @Groups({"wallet"})
*/
private $contract;
}
GET http://mon-ticketing-de-dev.com:81/v1/contracts/197
=>
{
"@context": "/v1/contexts/Contract",
"@id": "/v1/contracts/197",
"walletList": [
"/v1/wallets/165"
],
"minisiteList": [
{
"@id": "/v1/minisites/183",
"@type": "Minisite",
"slug": "toto",
"contract": "/v1/contracts/197",
}
],
}
How could I pass an entity to the FormType to populate a select list?
Hi,
we encounter performance issues with related entities.
Given we make this API call:
GET https://api/articles/12345678-4792-11e7-88db-f23c9124c2b0
Which returns :
{
"@context": "\/contexts\/Article",
"@id": "\/articles\/12345678-4792-11e7-88db-f23c9124c2b0",
"@type": "Article",
"id": "12345678-4792-11e7-88db-f23c9124c2b0",
"title": "foo bar",
"tags": [
"\/tags\/331a2c47-164a-11e7-8a8a-f23c9124c2b0",
"\/tags\/331a658c-164a-11e7-8a8a-f23c9124c2b0",
"\/tags\/331a71cc-164a-11e7-8a8a-f23c9124c2b0",
"\/tags\/9c828580-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9c82e2b1-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9c834c02-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9c850c8b-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9c86ca17-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9cc9f244-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9cd4d60e-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d186137-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d194643-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d1f1245-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d1fe224-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d33f4f4-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d37d44d-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d4353fe-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9d5d71fd-75f3-11e7-8820-f23c9124c2b0",
"\/tags\/9da0e8d6-75f3-11e7-8820-f23c9124c2b0"
]
}
When we iterate over the entity tags collection ($article->getTags()
), each iteration makes an API call to retrieve the tag entity:
GET https://api/tags/331a2c47-164a-11e7-8a8a-f23c9124c2b0
GET https://api/tags/331a658c-164a-11e7-8a8a-f23c9124c2b0
GET https://api/tags/331a71cc-164a-11e7-8a8a-f23c9124c2b0
GET https://api/tags/9c828580-164a-11e7-8a8a-f23c9124c2b0
...
Which kills the performances of our application.
Embedding the tags in the article API response is not an option in our case (for a lot of reasons).
So my question is: is there a way to tell the API client to retrieve all the tags at once instead of retrieving them one by one ?
if the setter method does not exists, the variable won't be assigned and there will be no error message
Hi, I would like to know if you plan to make a version of rest-client-sdk compatible with Symfony 3.0.
Thanks for the good work :-)
The EntityRepository find
method does registerClean
in the unit of work but every other call to UnitOfWork
is made with the entity id, which is probably better.
There might be a problem when having two different instances of an entity sharing the same id though, but the problem is already here as the use the id getter in the findBy
/ findAll
methods
Today, the datetime serializer format date like this : ->format('c')
, but sometimes, the API needs a custom format (SIBIL requires a Java Instant that doesn't accept anything else than Y-m-d\TH:i:s\Z
).
Allow the Attribute
to have an options
parameters, and the datetime should use options.format
value if provided
->findAll()
on the AbstractClient
results in an empty array.
I think this has to do with the hydra:
prefix on the member
key.
As far as I know the key for members on a collection is members
instead of hydra:member
see: convertList
method in the AbstractClient
class.
Add a ChangeSet to know what has changed before sending PUT requests to avoid overwriting values with entity default
Type hinting is a bit off on most classes of this project, which makes it difficult to use in recent IDEs.
class EntityRepository
{
// ...
/**
* @object REST Client
*/
protected $restClient;
/**
* @var SDK object
*/
protected $sdk;
// ...
/**
* EntityRepository constructor
*
* @param object $sdkClient - the client to connect to the datasource with
* @param object $restClient - client to process the http requests
* @param string $entityName The entiy to work with
*/
public function __construct($sdkClient, $restClient, $entityName)
{
$this->sdk = $sdkClient;
$this->restClient = $restClient;
$this->entityName = $entityName;
}
// ...
}
I could take care of this by adding/fixing PHPDoc blocks and methods signatures as such (for example):
class EntityRepository
{
// ...
/**
* @var RestClient
*/
protected $restClient;
/**
* @var SdkClient
*/
protected $sdk;
// ...
/**
* EntityRepository constructor
*
* @param SdkClient $sdkClient The client to connect to the datasource with
* @param RestClient $restClient The client to process the http requests
* @param string $entityName The entity to work with
*/
public function __construct(SdkClient $sdkClient, RestClient $restClient, $entityName)
{
$this->sdk = $sdkClient;
$this->restClient = $restClient;
$this->entityName = $entityName;
}
// ...
}
Just let me know if it's ok with you :)
We have the following entity classes:
<?php
namespace AppBundle\Entity;
use Mapado\RestClientSdk\Mapping\Annotations as Rest;
/**
* @Rest\Entity(key="articles")
*/
class Article
{
/**
* @Rest\Id
* @Rest\Attribute(name="id", type="string")
*/
private $id;
/**
* @Rest\ManyToOne(name="section", targetEntity="Section")
*/
private $section;
public function setId($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function setSection(Section $section)
{
$this->section = $section;
}
public function getSection()
{
return $this->section;
}
}
and
<?php
namespace AppBundle\Entity;
use Mapado\RestClientSdk\Mapping\Annotations as Rest;
/**
* @Rest\Entity(key="sections")
*/
class Section
{
/**
* @Rest\Id
* @Rest\Attribute(name="id", type="string")
*/
private $id;
/**
* @var string
*
* @Rest\Attribute(name="title", type="string")
*/
private $title;
public function setId($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
When we persist an entity like this:
// $section is a valid and existing entity fetched from the API.
$article = new Article();
$article->setSection($section);
$articleRepository = $sdk->getRepository('articles');
$articleRepository->persist($article);
Then the HTTP call to the API is like this:
Path: POST http://domain.tld/articles
Parameters: { "json": { "section": "946c3a2a-e36a-11e6-b8b0-0242ac110002", "title": "test" }, "version": "1.0", "headers": { "Accept-Language": "en-US,en;q=0.5" } }
Notice how the "section" key is "946c3a2a-e36a-11e6-b8b0-0242ac110002" instead of "/sections/946c3a2a-e36a-11e6-b8b0-0242ac110002"
I think the fix could be to modify the Serializer::recursiveSerialize method, around line 189, by replacing:
if ($data->getId()) {
$data = $data->getId();
} elseif (...) {
with:
if ($data->getId()) {
$hydrator = $this->sdk->getModelHydrator();
$data = $hydrator->convertId($data->getId(), $relation->getTargetEntity());
} elseif (...) {
Hi,
In an attempt to optimize a CMS using the mapado REST client SDK, I tried implementing HTTP-based normalization/denormalization groups on the API I'm using, i.e. :
GET /articles/1234
would return a complete entity like this:
{
"id": 1234,
"body": "<p>Article body</p>",
"template": "article",
"section": 2345,
"title": "Article title",
"sponsor": null,
"authors": [
456,
567
],
"tags": [
567,
678
],
"status": "PUBLISHED",
"createdAt": "2017-09-21T15:28:10-04:00",
"modifiedAt": "2017-10-23T15:40:04-04:00"
}
While
GET /articles/1234?_groups[]=bare
would return only a partial entity like this:
{
"id": 1234,
"title": "Article title"
}
Unfortunately, the REST client calls every setter on the entity, and for values which were not returned by the API, attempts to set NULL values with setters which don't allow them, causing fatal errors.
A possible "fix" / solution would be to skip setter calls for which the corresponding property was not provided in the API response.
Does it make sense?
At the moment your only lib which requires a minimum of php 5.5 is the guzzle client.
Could you run the tests with guzzle ~5.3.0
also, to check if the SDK still works on PHP 5.4?
Hi dude, how are you?
i found this really weird bug.
When i call a method on a Proxy, (proxy because Oficina its a subresource), in the controller like this:
$agenteRepository->find(20)->getOficina()
object(ProxyManagerGeneratedProxy_PM_\App\Entity\Rest\Oficina\Generated183fa80ff3d071671b476b73d35b700e)[2419]
private 'initializer9b768' =>
object(Closure)[2371]
public 'static' =>
array (size=4)
'sdk' =>
object(Mapado\RestClientSdk\SdkClient)[532]
...
'classMetadata' =>
object(Mapado\RestClientSdk\Mapping\ClassMetadata)[562]
...
'id' => string '/api/oficinas/1' (length=15)
'proxyModelName' => string 'App\Entity\Rest\Oficina' (length=23)
public 'this' =>
object(Mapado\RestClientSdk\SdkClient)[532]
protected 'restClient' =>
object(Mapado\RestClientSdkBundle\RequestAwareRestClient)[440]
...
protected 'cacheItemPool' =>
object(Symfony\Component\Cache\Adapter\ArrayAdapter)[493]
...
protected 'cachePrefix' => string 'mapado_rest_client_' (length=19)
private 'mapping' =>
object(Mapado\RestClientSdk\Mapping)[537]
...
private 'serializer' =>
object(Mapado\RestClientSdk\Model\Serializer)[531]
...
private 'modelHydrator' =>
object(Mapado\RestClientSdk\Model\ModelHydrator)[536]
...
private 'repositoryList' =>
array (size=1)
...
private 'proxyManagerConfig' =>
object(ProxyManager\Configuration)[534]
...
private 'unitOfWork' =>
object(Mapado\RestClientSdk\UnitOfWork)[526]
...
public 'parameter' =>
array (size=5)
'$proxy' => string '' (length=10)
'$method' => string '' (length=10)
'$parameters' => string '' (length=10)
'&$initializer' => string '' (length=10)
'$properties' => string '' (length=10)
private 'initializationTracker65ef4' => boolean false
private 'iri' (App\Entity\Rest\Oficina) => string '/api/oficinas/1' (length=15)
private 'numero' (App\Entity\Rest\Oficina) => null
private 'descripcion' (App\Entity\Rest\Oficina) => null
private 'finalidad' (App\Entity\Rest\Oficina) => null
private 'ofiPrgCodigo' (App\Entity\Rest\Oficina) => null
private 'baja' (App\Entity\Rest\Oficina) => null
private 'ofiUejCodigo' (App\Entity\Rest\Oficina) => null
private 'ofiModCodigo' (App\Entity\Rest\Oficina) => null
private 'codigo' (App\Entity\Rest\Oficina) => null
All the attributes of the entity are in null.
BUT! if you pass this same object to a template like this:
{{ agente.oficina.numero }}
this works.......
SO.....I investigated this and i found this:
vendor/mapado/rest-client-sdk/src/SdkClient.php:245
if (!$isAllowedMethod) {
this condition on the initializer function always return false when you call it from a controller....but i don't know why...
Hi,
is it possible to eventually update ocramius/proxy-manager in Composer ?
I ask because the actual version do not support PHP7 Scalar type declarations
http://php.net/manual/fr/migration70.new-features.php
Thanks again
If you have this entity:
{
"@id": "/foo/1",
"title": "first foo"
}
and you send the same, the UnitOfWork will try to put this:
{}
Which is useless. If the unit of work returns an empty diff, we should not send the request
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.