I have been trying out Symfony 2.2, the FOSRest Bundle (using JMS Serializer), and Doctrine ODM using MongoDB.
After many hours of trying to figure out how to correctly setup the FOSRest Bundle I'm still having some trouble: I have a very simple route that returns a list of products and prices. Whenever I request for the HTML format I get the correct response, but if I request any other format (JSON, XML) I get an error:
[{"message": "Resources are not supported in serialized data. Path: Monolog\\Handler\\StreamHandler -> Symfony\\Bridge\\Monolog\\Logger -> Doctrine\\Bundle\\MongoDBBundle\\Logger\\Logger -> Doctrine\\Bundle\\MongoDBBundle\\Logger\\AggregateLogger -> Doctrine\\ODM\\MongoDB\\Configuration -> Doctrine\\MongoDB\\Connection -> Doctrine\\ODM\\MongoDB\\LoggableCursor",
"class": "JMS\\Serializer\\Exception\\RuntimeException",...
you can see the full error message here
My current setup is very simple: I have created a single route to a controller that returns a list of products and the price (I followed this example to create the product document).
This is the route:
rest_product:
type: rest
resource: Onema\RestApiBundle\Controller\ProductController
This is the controller:
<?php
namespace Onema\RestApiBundle\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\Rest\Util\Codes;
use JMS\Serializer\SerializationContext;
use Onema\RestApiBundle\Document\Product;
class ProductController extends FOSRestController implements ClassResourceInterface
{
public function getAction()
{
$dm = $this->get('doctrine_mongodb')->getManager();
$products = $dm->getRepository('RestApiBundle:Product')->findAll();
if(!$products)
{
throw $this->createNotFoundException('No product found.');
}
$data = array('documents' => $products);
$view = $this->view($data, 200);
$view->setTemplate("RestApiBundle:Product:get.html.twig");
return $this->handleView($view);
}
}
This is the view called from the controller Resources/Product/get.html.twig:
<ul>
{% for document in documents %}
<li>
{{ document.name }}<br />
{{ document.price }}
</li>
{% endfor %}
</ul>
Any ideas why this would work correctly for one format but not the others? Anything additional I'm supposed to setup?
UPDATE:
This is the config values I have been using.
At the end of app/config/config.yml I had this:
sensio_framework_extra:
view: { annotations: false }
router: { annotations: true }
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
formats:
json: true
failed_validation: HTTP_BAD_REQUEST
default_engine: twig
view_response_listener: 'force'
WORKAROUND:
Doing a bit more research I ran into another error which lead me to this questions and answer:
https://stackoverflow.com/a/14030646/155248
Once I got rid of the Doctrine\ODM\MongoDB\LoggableCursor by adding every result to an array like this:
$productsQ = $dm->getRepository('RestApiBundle:Product')->findAll();
foreach ($productsQ as $product) {
$products[] = $product;
}
return $products;
I started getting the results in the correct format. This is kind of a lame solution and still hope to find a better answer to this issue.
If you want to get a colection of RestApiBundle:Product documents, you MUST call the method "toArray" after calling find method from repository or getQuery method from query builder
/**
* #Route("/products.{_format}", defaults={"_format" = "json"})
* #REST\View()
*/
public function getProductsAction($_format){
$products = $this->get('doctrine_mongodb')->getManager()
->getRepository('RestApiBundle:Product')
->findAll()->toArray();
return $products;
}
also you can call array_values($products) for correct serialization of exclude strategy
Most likely the error lies somewhere in your config files or perhaps lack of? Add your configs and if I can I will update my answer.
For now I'll walk you through a simple implementation.
First lets start with the configs:
Note: I'm going to be using annotations for some of the settings see SensioFrameworkExtraBundle.
#app/config/config.yml
sensio_framework_extra:
view:
annotations: false
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
view_response_listener: 'force'
First we setup sensio extra bundle. The default config enables annotations are set to true. I disabled annotations for the view(I won't be using them here).
For fos_rest we are setting up the Listeners, we're keeping it simple so we're going to use the example from their docs.
We will create the entity:
<?php
namespace Demo\DataBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints;
use JMS\Serializer\Annotation\ExclusionPolicy; //Ver 0.11+ the namespace has changed from JMS\SerializerBundle\* to JMS\Serializer\*
use JMS\Serializer\Annotation\Expose; //Ver 0.11+ the namespace has changed from JMS\SerializerBundle\* to JMS\Serializer\*
/**
* Demo\DataBundle\Entity\Attributes
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Demo\DataBundle\Entity\AttributesRepository")
*
* #ExclusionPolicy("all")
*/
class Attributes
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*
* #Expose
*/
private $id;
/**
* #var string $attributeName
*
* #ORM\Column(name="attribute_name", type="string", length=255)
*
* #Expose
*/
private $attributeName;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set attributeName
*
* #param string $attributeName
* #return Attributes
*/
public function setAttributeName($attributeName)
{
$this->attributeName = $attributeName;
return $this;
}
/**
* Get attributeName
*
* #return string
*/
public function getAttributeName()
{
return $this->attributeName;
}
}
You will notice a couple of annotation settings. First we set #ExclusionPolicy("all") then we manually set which object we want to #Expose to the API. You can find out more about this here and a list of JMS Serializer annotations
Now lets move on to a simple controller:
<?php
namespace Demo\DataBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest; //Lets use annotations for our FOSRest config
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\Rest\Util\Codes;
use Symfony\Component\HttpFoundation\Request;
use Demo\DataBundle\Entity\Attributes;
class AttributesController extends FOSRestController implements ClassResourceInterface
{
/**
* Collection get action
* #var Request $request
* #return array
*
* #Rest\View()
*/
public function cgetAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('DemoDataBundle:Attributes')->findAll();
return array(
'entities' => $entities,
);
}
}
This is a simple controller that will return everything.
Hopefully this helped. I think your error is coming from a bad setting related to the Serializer. Make sure your exposing some data.
Related
I'm playing new Symfony 4. I really like when service use autowire and the component use annotations. I don't must nothing registration in services.yml. Ofcourse I can't solve everything in this way, but I try.
I create simple and test project. When I have Page. Page has "slug" field. This value is generated with field "title". I created special service (App\Service\Slug).
I would like to generate slug in event when page is create or update. To the event I should be to injected the slug service.
I would like to create one listner for only one entity - Page. I found: https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html In this solution the event is run for all entities and I must create configuration in services.yml.
App\EventListener\PageListener:
tags:
- { name: doctrine.event_listener, event: prePersist }
class PageListener
{
/**
* #var Slug
*/
protected $slug;
/**
* PageListener constructor.
*
* #param Slug $slug
*/
public function __construct(Slug $slug)
{
$this->slug = $slug;
}
public function prePersist(
LifecycleEventArgs $args
) {
$page = $args->getObject();
/** ... */
$slug = $this->slug->get($page->getTitle());
$page->setSlug($slug);
/** ... */
}
}
In this solution the event is run in all enitites and I must check the instance.
I remeber that In sf2.8 I could add adnotation to entity (EntityListeners) -> https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html In this version symfony I see this adnotation but it doesn't work. I found only old cookbook: https://symfony.com/doc/master/bundles/DoctrineBundle/entity-listeners.html
How should I do it correctly?
In the conclusion I want to create a simple doctrine event, which work for only one entity and its configuration is minimal.
edit:
When I tested, I found solution which "works":
Entity:
/**
* Class Page
*
* #package App\Entity
*
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\EntityListeners("App\EventListener\PageListener")
* #ORM\Table(name="pages")
*/
class Page
services.yaml:
App\EventListener\PageListener:
tags:
- { name: doctrine.orm.entity_listener }
PageListener:
public function __construct(Slug $slug)
{
$this->slug = $slug;
}
/**
* #ORM\PrePersist
*/
public function prePersistHandler(Page $user, LifecycleEventArgs $event)
I don't know how or I can't removed configuration in "service.yaml".
I'm using EasyAdminBundle for entity management and to upload images I want to useVichUploaderBundle.
Following the documentation configure the Bundle:
https://github.com/javiereguiluz/EasyAdminBundle/blob/master/Resources/doc/integration/vichuploaderbundle.rst
I do not use annotations but yml as described in the documentation:
https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/mapping/yaml.md
My code looks like this:
//app/config/config.yml
vich_uploader:
db_driver: orm
mappings:
torneo_images:
uri_prefix: '%app.path.torneo_images%'
upload_destination: '%kernel.root_dir%/../web/uploads/images/torneos'
..........
easy_admin:
form:
fields:
- logo
- { property: 'imageFile', type: 'file' }
The yml configuration file:
//BackendBundle/Resources/config/doctrine/Torneos.orm.yml
......
logo:
type: string
nullable: true
length: 255
options:
fixed: false
imageFile:
mapping: torneo_images
filename_property: logo
Add to Entity
//BackendBundle/Entity/Torneos.orm.yml
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\PropertyMapping as Vich;
namespace BackendBundle\Entity;
.......
/**
* #var string
*/
private $logo;
/**
* #var File
*/
private $imageFile;
.......
/**
* Set logo
*
* #param string $logo
*
* #return Torneos
*/
public function setLogo($logo)
{
$this->logo = $logo;
return $this;
}
/**
* Get logo
*
* #return string
*/
public function getLogo()
{
return $this->logo;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* #return Torneos
*/
public function setImageFile(File $logo = null)
{
$this->imageFile = $logo;
// VERY IMPORTANT:
// It is required that at least one field changes if you are using Doctrine,
// otherwise the event listeners won't be called and the file is lost
//if ($image) {
// if 'updatedAt' is not defined in your entity, use another property
// $this->updatedAt = new \DateTime('now');
//}
return $this;
}
/**
* #return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
Also add this code (I'm not sure if it's correct)
//BackendBundle/Resources/config/vich_uploader/Torneos.orm.yml
BackendBundle\Entity\Torneos:
imageFile:
mapping: torneo_images
filename_property: logo
Can anyone give me some idea to fix it?
The solution was quite simple.
The error occurs because the use are placed before thenamespace in the controller.
namespace BackendBundle\Entity;
Regards
I have an entity BaseInformation
/**
* #ORM\Entity
* #ORM\EntityListeners({"AppBundle\EntityListener\BaseInformationListener"})
* #ORM\Table(name="BaseInformation")
*/
class BaseInformation
{ ...
Therefore I have an EntityListener
/**
* Class BaseInformationListener
* #package AppBundle\EventListener
*/
class BaseInformationListener
{
/**
* #ORM\PreUpdate
*
* #param BaseInformation $baseInformation
* #param PreUpdateEventArgs $event
*/
public function preUpdateHandler(BaseInformation $baseInformation, PreUpdateEventArgs $event)
{
dump($baseInformation);
dump($event->getEntityChangeSet());
}
}
I need to save the ChangeSet into the database. But I have no access to an EntityManager. I can make a service out of it, but the listener is automatically called over the annotation in the entity. So how do I have access to the EntityManager to save my ChangeSet for example?
You can define the listener a service and tag it as an EntityListener so you can use the dependency you need as usually:
services:
base_information_listener:
class: AppBundle\EntityListener\BaseInformationListener
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: doctrine.orm.entity_listener }
Prior to doctrine 2.5 you need to use the annotation in the related entity also (as described in the doc).
Hope this help
Symfony 3.1.7 + FOSRestBundle latest version
<?php
namespace PM\ApiBundle\Controller;
...
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
class ArticlesController extends FOSRestController
{
/**
* #ApiDoc(
* section="articles",
* resource=true,
* description="Get articles published"
* )
* #Rest\View(serializerGroups={"article"})
* #Rest\Get("/articles")
*/
public function getArticlesAction(Request $request)
{
$articles = $this->getDoctrine()
->getManager()
->getRepository('PMPlatformBundle:Article')
->findAllDateDesc();
/* #var $articles Article[] */
return $articles;
}
Then in my Article entity I added this annotation #Groups({"article"}) with the right use statement.
Whit default serializer I get :
[
[],
[]
]
Whit JMS serializer (bundle) I get :
{
"0": {},
"1": {}
}
(I have two articles in db)
it seems like the "article" group is not recognized. When I use the default serializer whithout this annotations I get a circular error.
What's wrong ?
[EDIT] Same behavior with
/**
* #ApiDoc(
* section="articles",
* resource=true,
* description="Get articles published"
* )
* #Rest\View()
* #Rest\Get("/articles")
*/
public function getArticlesAction(Request $request)
{
$context = new Context();
$context->addGroup('article');
$articles = $this->getDoctrine()
->getManager()
->getRepository('PMPlatformBundle:Article')
->findAllDateDesc();
/* #var $articles Article[] */
$view = $this->view($articles, 200);
$view->setContext($context);
return $view;
}
The response still empty.
You can keep the default serializer of symfony. No need of JMSSerializer.
You may have forgotten to activate the annotations of serializer in config.yml (https://symfony.com/doc/current/serializer.html#using-serialization-groups-annotations)
#app/config/config.yml
framework:
....
serializer: { enable_annotations: true }
It is necessary to force the view_response_listener in config.yml (http://symfony.com/doc/master/bundles/FOSRestBundle/3-listener-support.html, http://symfony.com/doc/master/bundles/FOSRestBundle/view_response_listener.html)
#app/config/config.yml
fos_rest:
view:
view_response_listener: 'force'
That should work !
Ok I fixed it using JMS serializer like this :
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
class ArticlesController extends FOSRestController
{
/**
* #ApiDoc(
* section="articles",
* resource=true,
* description="Get articles published"
* )
* #Rest\View()
* #Rest\Get("/articles")
*/
public function getArticlesAction(Request $request)
{
$serializer = SerializerBuilder::create()->build();
$articles = $this->getDoctrine()
->getManager()
->getRepository('PMPlatformBundle:Article')
->findAllDateDesc();
/* #var $articles Article[] */
return $serializer->serialize($articles, 'json', SerializationContext::create()->setGroups(array('article')));
}
Now the groups annotations works fine.
try php bin/console cache:clear
Using your suggested answer
$serializer = SerializerBuilder::create()->build();
return $serializer->serialize($collection, 'json', SerializationContext::create()->setGroups(['group']));
returns JSON as string to me. To solve your problem, you have to have the following in your config.yml file:
jms_serializer:
metadata:
auto_detection: true
this is how it recognises mappings that you specify with your annotations in Entity files. More info on it here: docs. Having that, you can use the #View(serializerGroups={"group"}) annotation as you wanted.
P.S. Symfony 3.2.2; JMSSerializer 1.1; FOSRestBundle 2.1
Is it possible to override #ManyToOne(targetEntity)?
I read this Doctrine documentation page, but it doesn't mention how to override targetEntity.
Here's my code:
namespace AppBundle\Model\Order\Entity;
use AppBundle\Model\Base\Entity\Identifier;
use AppBundle\Model\Base\Entity\Product;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\AttributeOverrides;
use Doctrine\ORM\Mapping\AttributeOverride;
/**
* Class OrderItem
*
*
* #ORM\Entity
* #ORM\Table(name="sylius_order_item")
* #ORM\AssociationOverrides({
* #ORM\AssociationOverride(
* name="variant",
* joinColumns=#ORM\JoinColumn(
* name="variant", referencedColumnName="id", nullable=true
* )
* )
* })
*/
class OrderItem extends \Sylius\Component\Core\Model\OrderItem
{
/**
* #var
* #ORM\ManyToOne(targetEntity="AppBundle\Model\Base\Entity\Product")
*/
protected $product;
/**
* #return mixed
*/
public function getProduct()
{
return $this->product;
}
/**
* #param mixed $product
*/
public function setProduct($product)
{
$this->product = $product;
}
}
I was able to to override the definition for the "variant" column and set this column to null, but I can't figure it out how to change the targetEntity.
As described in the doc, you cannot change the type of your association:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#association-override
BUT, you can define a targetEntity as an interface (that's the default sylius conf it seems),
targetEntity="AppBundle\Entity\ProductInterface"
Extends the original one in your Interface file
namespace AppBundle\Entity;
use Sylius\Component\Core\Model\ProductInterface as BaseProductInterface;
interface ProductInterface extends BaseProductInterface {}
And add the mapping in your configuration
doctrine:
orm:
resolve_target_entities:
AppBundle\Entity\ProductInterface: AppBundle\Entity\Product
It's described here: http://symfony.com/doc/current/doctrine/resolve_target_entity.html
Hope it helps
I didn't find a way to override the targetEntity value of an association when using annotations, but it is possible when using PHP mapping (php or staticphp in Symfony).
https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/php-mapping.html
We can create a function:
function updateAssociationTargetEntity(ClassMetadata $metadata, $association, $targetEntity)
{
if (!isset($metadata->associationMappings[$association])) {
throw new \LogicException("Association $association not defined on $metadata->name");
}
$metadata->associationMappings[$association]['targetEntity'] = $targetEntity;
}
and use it like this:
updateAssociationTargetEntity($metadata, 'product', AppBundle\Model\Base\Entity\Product::class);
That way, when Doctrine loads the metadata for the class AppBundle\Model\Order\Entity\OrderItem, it first loads the parent (\Sylius\Component\Core\Model\OrderItem) metadata, and then it loads the PHP mapping for the child class where we override the association mapping that was set when loading the parent class metadata.