This is related to my other question: Persisting entities using a REST API.
For a project in Symfony2 I need to be able to persist entities using an remote (third-party) RESTful API. I also want to be able to retrieve entities with data from that API.
In other words, my objects are saved in the third-party database. They are not saved in my own database. Whenever I need to save data, or find data, I use their REST API.
I have been pointed to several libraries, including one made by Doctrine itself. However, none of them offers me what I'm looking for. The one made by Doctrine is the best option, but uses the Active Record pattern and doesn't offer all the sweet Doctrine 2 stuff. Don't get me wrong, I've been using Active Record implementations for a long time, but I've fallen in love with Doctrine's Data Mapper pattern now.
Ideally, I'd like to be able to use Doctrine's ORM and simply replace the database-specific part with logic that saves entities using an API call. (and of course retrieves them using that same API). This way I can save my entities using roughly the same syntax:
// current way to save $entity in database:
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
// desired way to save $entity using REST API:
// (just an example, it doesn't have to be exactly like this)
$em = $this->getDoctrine()->getManager('rest');
$em->persist($entity);
$em->flush();
Note that I'm not trying to build my own API, I'm simply trying to communicate with a third party API in order to save my entities. I'm relatively new to Doctrine, but I'm liking it so far. I really like the idea of seperating the persistence logic from the entities, but so far I can't find out how I can use that to save them using an API.
There is an article in Symfony's documentation, describing how to work with multiple Entity Managers. I'm looking for a solution similar to this, but with an entity manager that enables me to use REST instead of the DB.
I've been trying to tweak Doctrine's ORM myself, but I only end up rewriting half their code because it (seems to be) too tightly coupled to the Database-specific logic. I might be doing something stupid of course.
So my question is, is there a way to replace / override the database-specific parts of Doctrine's ORM with custom ones? Without rewriting a lot of things that should be common for all persistence methods? Has it been done before? Or is it simply not possible because Doctrine is intended for use with a database and isn't flexible enough for other uses?
My own progress
CakePHP seems to be able to do this, by letting you define a custom DataSource. This way you can save your models using an SQL database, but also using an API, sessions, etc. I want to do roughly the same, but using Doctrine instead of CakePHP.
Update 1
The actual database queries seem to be executed by the
Doctrine\ORM\Persisters\BasicEntityPersister class. There are several other xxxPersister classes, to deal with different types of inheritance. It might be possible to replace the xxxPersister classes with our own, so we can replace the DB code with REST API code.
The persister objects are created within the getEntityPersister() method of the Doctrine\ORM\UnitOfWork class. The classnames are hardcoded so we need to override Doctrine\ORM\UnitOfWork if we want to use our own persisters.
Update 2
Doctrine\ORM\UnitOfWork seems to be hardcoded into Doctrine\ORM\EntityManager, so we need to override that one as well. However, this class seems to contain some database-specific parts. For instance, it's constructor requires a Doctrine\DBAL\Connection object as parameter. Perhaps it's better to create our own EntityManger (implementing the Doctrine\Common\Persistence\ObjectManager interface), as long as that doesn't take too much time / effort.
Update 3
The database-specific code for retrieving/loading/finding objects lives in the same class as the code for persisting / deleting etc: the Doctrine\ORM\Persisters\xxxPersister classes. So if we are able to replace them with our own, in order to persist objects, we can retrieve objects as well. When you call $entityRepository->findAll(), for instance, it will return $entityRepository->findBy(array()) (because findAll() is simply an alias for findBy(array())) which will run the following code:
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->loadAll($criteria, $orderBy, $limit, $offset);
In other words, once we get EntityManager to create the right UnitOfWork and xxxPersister objects, we will be able to use the find methods in the EntityRepository.
Update 4
I discovered that a new feature is developed for Doctrine: custom persisters (also see this). This should make it easier to use a custom persister class. I don't know yet if it will enable us to create a non-DB persister, but it looks promising. However, the last updates were in August, so I'm not sure if it's still in active development.
You might use https://github.com/doctrine/rest to build a REST client, which talks to the target server. The essential part here is the mapping from entity (local) to REST API (target).
In short: Doctrine2 (local DB) -> Rest client (entity to rest mapping) -> Request (target server)
Doctrine/Rest provides also the other way around: a Doctrine Rest Server, to expose your local entities via REST (requests to your server). But thats not what you are looking for.
DoctrineRestDriver is exactly doing what you are looking for.
https://github.com/CircleOfNice/DoctrineRestDriver
Configure Doctrine:
doctrine:
dbal:
driver_class: "Circle\\DoctrineRestDriver\\Driver"
host: "http://www.your-url.com/api"
port: 80
user: "Circle"
password: "CantRenember"
Build entity:
/**
* This annotation marks the class as managed entity:
*
* #ORM\Entity
*
* You can either only use a resource name or the whole url of
* the resource to define your target. In the first case the target
* url will consist of the host, configured in your options and the
* given name. In the second one your argument is used as it is.
* Important: The resource name must begin with its protocol.
*
* #ORM\Table("products|http://www.yourSite.com/api/products")
*/
class Product {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
public function getId() {
return $this->id;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
}
Let's assume we have used the value http://www.yourSite.com/api/products for the product entity's #Table annotation.
Controller:
<?php
namespace CircleBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\HttpFoundation\Response;
class UserController extends Controller {
/**
* Sends the following request to the API:
* POST http://www.yourSite.com/api/products HTTP/1.1
* {"name": "Circle"}
*
* Let's assume the API responded with:
* HTTP/1.1 200 OK
* {"id": 1, "name": "Circle"}
*
* Response body is "1"
*/
public function createAction() {
$em = $this->getDoctrine()->getManager();
$entity = new CircleBundle\Entity\Product();
$entity->setName('Circle');
$em->persist($entity);
$em->flush();
return new Response($entity->getId());
}
/**
* Sends the following request to the API by default:
* GET http://www.yourSite.com/api/products/1 HTTP/1.1
*
* which might respond with:
* HTTP/1.1 200 OK
* {"id": 1, "name": "Circle"}
*
* Response body is "Circle"
*/
public function readAction($id = 1) {
$em = $this->getDoctrine()->getManager();
$entity = $em->find('CircleBundle\Entity\Product', $id);
return new Response($entity->getName());
}
/**
* Sends the following request to the API:
* GET http://www.yourSite.com/api/products HTTP/1.1
*
* Example response:
* HTTP/1.1 200 OK
* [{"id": 1, "name": "Circle"}]
*
* Response body is "Circle"
*/
public function readAllAction() {
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('CircleBundle\Entity\Product')->findAll();
return new Response($entities->first()->getName());
}
/**
* After sending a GET request (readAction) it sends the following
* request to the API by default:
* PUT http://www.yourSite.com/api/products/1 HTTP/1.1
* {"name": "myName"}
*
* Let's assume the API responded the GET request with:
* HTTP/1.1 200 OK
* {"id": 1, "name": "Circle"}
*
* and the PUT request with:
* HTTP/1.1 200 OK
* {"id": 1, "name": "myName"}
*
* Then the response body is "myName"
*/
public function updateAction($id = 1) {
$em = $this->getDoctrine()->getManager();
$entity = $em->find('CircleBundle\Entity\Product', $id);
$entity->setName('myName');
$em->flush();
return new Response($entity->getName());
}
/**
* After sending a GET request (readAction) it sends the following
* request to the API by default:
* DELETE http://www.yourSite.com/api/products/1 HTTP/1.1
*
* If the response is:
* HTTP/1.1 204 No Content
*
* the response body is ""
*/
public function deleteAction($id = 1) {
$em = $this->getDoctrine()->getManager();
$entity = $em->find('CircleBundle\Entity\Product', $id);
$em->remove($entity);
$em->flush();
return new Response();
}
}
You can even use DQL or native queries.
As a ready-to-use solution wasn't available, I decided to write my own. I called it RAPL. It's heavily inspired by Doctrine's ORM (in fact, it uses many of the interfaces provided by Doctrine Common).
Using RAPL I can simply write a small YAML file to configure the mapping between my entities and the web service, allowing me to persist/retrieve entities using the custom EntityManager.
I think you are in not right way.
I'm not ready to dig into the documentation now, but I understand doctrine stack as:
ORM -> DQL (doctrine query language) ->dbal ->Some database sql
And point for implementation you feature in DBAL as custom database driver.
I think create common REST-Driver realy interesting feature and it will do easy integration with third-party services.
I'm not sure, but you can try to use lifecycle callback events for entities to perform persisting logic via REST.
I wanted to do a similar thing, so I built this library to help expose doctrine entities as RESTful resources. It has a fair amount of features, and allows you to define exactly what you want to have exposed via both pull (GET) and push (POST/PUT/PATCH) methods.
http://leedavis81.github.io/drest/
https://github.com/leedavis81/drest
Hope it helps
Related
I have two Laravel sites, a client and a server. The client connects to the server, which (among other things) provides a RESTful api for a database. While this setup may seem a bit convoluted and superfluous, it is necessary for the use-case.
How can I set up the client so that it uses the server's API to interact with resources?
For instance, the sever provides organisation models at an /organisations/{id} end point. If I wanted to display them all on the client, and provide a form for updating each organisation, is there a Laravel-esque way of doing this? Or is it something that'll end up being hand-rolled and hacky?
You have to use the http-client. Laravel provides already one, based on Guzzle.
So, in order to update something You have to create PUT route on Your Server and then just call it from Client like this:
$response = Http::put("https://YOUR.SERVER/organisations/$id", [
'name' => 'Steve',
'role' => 'Network Administrator',
]);
More info, You will find in Laravel Documentation
if you want to use the Laravel as a frontend client to consume a remote API it easy, a part the fact that you can consider the use of vue.js as your frontend in order to keep things simpler.
FRONTEND
Create the Laravel project as usual, the only difference is that you don't need to set up the database and migrations
You are going to use the normal MVC pattern here, so in your Frontend FlightController class, you will fetch the data like this.
getFlights()
{
$response = Http::get(env('API_URL') . '/flights')->json();
}
we can optionally install the package spatie/data-transfer-object
in order to convert your json data to DTO object like this:
getFlights()
{
$flights_data = Http::get(env('API_URL') . '/flights')->json();
$filghts = [];
foreach($flight_data as data)
$filghts->add(new \App\Models\Dto\FlightDto($data));
return view('flights.search-result', compact('filghts'));
}
the DTO class looks like this:
use Spatie\DataTransferObject\DataTransferObject;
class FlightDto extends DataTransferObject
{
/** #var integer|null */
public $id;
/** #var string|null */
public $flight_number;
}
The authentication is little bit tricky, refer to this question to see how you can create custom user provider.
BACKEND
Create a Laravel project, set up database and all your migrations
place all your routes in the default location api.php
Route::get('/trips', 'ApiTripsController#getFlights');
3- in the FlightsController do the following
public getFlights
{
return Flight::all() // where the Flight class is the Eloquent model
}
do not forget to provide a security layer to protect ressources from the server side
I have a problem with Api Platform and custom collection operation, when I need to manually require an argument in the route.
My first need is to GET on this route: query/userjob/[USER UUID] and retrieve a collection of all jobs for the given user.
My second need is to be able to GET on query/userjob/[USER UUID]/[JOB UUID] and retrieve details for the given user's job.
It might be important to say that I have no Api resource nor entity User, so I exclude all kind of subresource mapping or query.
So, let's say i have a UserJob ApiResource mapped as below:
App\Domain\User\Projection\UserJob:
itemOperations:
get:
method: 'GET'
path: '/userjob/{userId}/{jobId}'
requirements:
userId: '%uuid_regex%'
jobId: '%uuid_regex%'
collectionOperations:
get:
method: 'GET'
path: '/userjob/{userId}'
requirements:
userId: '%uuid_regex%'
attributes:
route_prefix: "/query"
In the class, I have:
final class UserJob
{
public $id; //int Auto inc
public $userId; //a UUID
public $jobId; //a UUID
public function __construct($userId, $jobId)
{
$this->userId = $userId;
$this->jobId = $jobId;
}
public function getId(): int
{
return $this->id;
}
public function getUserId()
{
return $this->userId;
}
public function getJobId()
{
return $this->jobId
}
I built a custom data provider for this class, in which I wrote the way to get the resource from the giver parameter (userId):
public function getCollection(string $resourceClass, string $operationName = null)
{
$userId = $this->request->getCurrentRequest()->attributes->get('userId');
return $this->repository->entityManager->getRepository($resourceClass)->findByUserId($userId);
}
When i make a GET call to, let's say, query/userjob/148e3200-f793-447b-bde8-af6b7b27372c it throws an exception:
Unable to generate an IRI for App\Domain\User\Projection\UserJob
And if I debug deeper, in the IRIConverter class, I find that the original exception is thrown from Router:
Some mandatory parameters are missing ("userId") to generate a URL for route "api_user_jobs_get_collection".
Nevertheless, if i dump the result of $this->repository->entityManager->getRepository($resourceClass)->findByUserId($userId);, all the elements that i'm looking for are well fetched from database.
So my intuition is that somehow ApiPlatform process fails to build the collection IRI that we usually can find at the beginning of the payload, and which in my case would be query/userjob/148e3200-f793-447b-bde8-af6b7b27372c.
And it fails while on the normalization or serialization process, because the "extra" param of my custom operation (the user UUID) is not passed to the collection normalizer, iri converter classes, so it has no way to give to the router the missing param to build the "api_user_jobs_get_collection" route.
What am I missing here? Is this a well-known problem that has a readymade solution that I missed ?
Or do I have to look for:
decorate the IRI converter?
use a custom normalizer?
do something with composite ids?
something else?
Your use case may have more solutions, and it depends what is preferred:
decorate iri converter, as you are using identifier in collection and this is not supported out of the box by API platform, as per my knowledge. And this is best choice if url like this are the style of your api.
use custom controller action with custom url style (docs: https://api-platform.com/docs/core/controllers/#creating-custom-operations-and-controllers), best if this is rare url in your api
Annotate your ids as identifiers in your api resource class (doc: https://api-platform.com/docs/core/identifiers/#custom-identifier-normalizer)
/**
* #var Uuid
* #ApiProperty(identifier=true)
*/
public $code;
but I haven't tried this with multiple ids, and this may work only for item url.
You may try to use custom data provider (docs: https://api-platform.com/docs/core/data-providers/). This will need to be done per resource or globally (supports() method) and you will need somehow (regex?) extract ids from url from $context array in getCollection() and getItem() methods. But as ApiPlatform will try to generate item iri, you may still end up with decorating iri converter.
Note: Using id in collection url may lead to other problems, like OpenAPI documentation generation. You may consider if what you want is not filtering of collection by "id" field, nicely supported, or retrieving collection of only "your" items. Which can be done by your data provider injecting security or by doctrine query extensions if someone uses doctrine.
Assumed I have a normal Symfony entity "Car", a self written service "Log" and I want to build a rest route
PUT http://mysite.de/api/{version}/cars/{oldcar}
to do something with a car given in "oldcar" stored in my database. In the body of the PUT there comes another Car object in JSON notation with the new data. Then I have following method:
/**
* #Rest\Put("/api/{version}/cars/{oldcar}")
* #ParamConverter("oldcar", class="MyBundle\Entity\Car")
* #ParamConverter("newcar", class="MyBundle\Entity\\Car", converter="fos_rest.request_body")
*/
public function putAction(Log $logger, EntityManagerInterface $em, Car $oldcar, Car $newcar)
{
... update the oldcar in the database with data from the newcar ...
}
I got this allready working!
But now I want to know: Is there a configuration of the FosRestBundle to omit the #ParamConverter annotations? In my opinion there are enough information for the framework to do this automatically without them.
Log and EntityManagerInterface can be found in the service container. oldcar's and newcar's types are defined in the method signature. Primary key for database access for oldcar is given by the route. There is only the JSON data in the body left for newcar.
Does anybody here get this working without the #ParamConverter annotations?
Please tell me if and a short idear how.
I'm using Symfony 2.6 and the FOS Rest Bundle.
Param converters for PATCH , DELETE and GET requests work nicely and reduce the code in the controller actions. However for POST requests I have a problem. The default \Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter gets called every time. This results in an exception:
Unable to guess how to get a Doctrine instance from the request information.
I checked the \Sensio\Bundle\FrameworkExtraBundle\EventListener\ParamConverterListener and saw that it's always including the Doctrine param converter in the onKernelController method. From the documentation it seems that the doctrine param converter is automatically applied for all type hinted controller actions unless you set it to off:
sensio_framework_extra:
request:
converters: true
auto_convert: false
I found a kind of hacky way to prevent this. The array of param converters to be applied will be indexed by the name of the type hinted argument in the controller method (which symfony gets by reflection). If I just name my param converter the same as this type hint then the default doctrine param converter will not be added to the list of param converters. For example:
...
* #ParamConverter(
* "MyEntity",
* class="Foo\Bar\MyEntity",
* converter="my_custom_converter"
* )
*
* #param MyEntity $myEntity
* #return MyEntity
*/
public function postMyEntityAction(MyEntity $myEntity)
{
I sort of wrote this question as I was digging deeper into the code and I'm not even really sure what my question is anymore. I guess it's "Is it logical to apply multiple param converters?" or would also like to know if it's possible to turn off param converters for certain actions. Maybe my logic is completely wrong here and this isn't what param converters were intended for.
I'd appreciate any advice.
Alright, I realized where I was going wrong. It was a simple case of not returning true from my custom paramConverter apply method. If it does return true then the doctrine param converter won't be applied.
When retrieving models from database and sending them to the client I want to include for each model the url to that resource.
Let's take as example this Article model, with:
id
title
content
etc.
Storing the url to an article in the DB doesn't make sense, because it can be easily made up from id and title:
ex: http://www.example.com/articles/article_id/article_title
So, this is what I am doing now:
I use the $appends array:
/**
* Additional attributes
*
* #var array
*/
protected $appends = array('article_url');
and created a getter for article_url:
/**
* Get the article url attribute
*
* #return string
*/
protected function getArticleUrlAttribute()
{
return $this->exists
? url('articles', $parameters = array(
$this->getKey(),
Str::title(Str::limit($this->title, 100))
))
: null;
}
This works just fine. The problem is, that probably the model should not include any logic for creating urls. What is a good approach for this problem? Where should I create the
url to the article before sending it to the client?
That sort of logic would usually go in whatever your framework's routing engine is. For instance, since it sounds like you're using Laravel, you'd probably make a Named Route -- call it, say, "canonical_article".
Then you can use the link_to_route helper to have your framework generate the URL.