I'm using the Gedmo SoftDeletable filter for Symfony2 and Doctrine (https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/softdeleteable.md)
I'm also using the JMSSerializerBundle to serialize reponses to JSON for my REST API.
How to tell jms serializer annotation group about softdeleteable filters ?
And when my response contain entity which have relation with entity which have deleted_at field not empty I have error
Entity of type 'AppBundle\Entity\Courses' for IDs id(2) was not found
because sub_cources, example id 1 have relation with courses in example id 2 and courses with id 2 have not empty deleted_at - removed entity
example I have
/**
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="sub_cources")
*#ORM\Entity(repositoryClass="AppBundle\Entity\Repository\SubCoursesRepository")
* #Gedmo\SoftDeleteable(fieldName="deletedAt")
* #AssertBridge\UniqueEntity(
* groups={"post_sub_course", "put_sub_course"},
* fields="name",
* errorPath="not valid",
* message="This name is already in use."
* )
*/
class SubCourses
/**
* #var Courses
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Courses", inversedBy="subCourses")
* #Annotation\Groups({
* "get_sub_courses"
* })
* #Annotation\Type("AppBundle\Entity\Courses")
*/
private $courses;
my action
return $this->createSuccessResponse(
[
'sub_courses' => $subCourses->getEntitiesByParams($paramFetcher),
'total' => $subCourses->getEntitiesByParams($paramFetcher, true),
],
['get_sub_courses'],
true
);
And my response look like
/**
* #param $data
* #param null|array $groups
* #param null|bool $withEmptyField
*
* #return View
*/
protected function createSuccessResponse($data, array $groups = null, $withEmptyField = null)
{
$context = SerializationContext::create()->enableMaxDepthChecks();
if ($groups) {
$context->setGroups($groups);
}
if ($withEmptyField) {
$context->setSerializeNull(true);
}
return View::create()
->setStatusCode(self::HTTP_STATUS_CODE_OK)
->setData($data)
->setSerializationContext($context);
}
How to tell jms serializer annotation group about softdeleteable filters ?
The solution to this issue that I used was to use the serializer events for pre and post serialization.
The first thing I did was have my entity implement the \Gedmo\SoftDeleteable\SoftDeleteable interface.
I created a container-aware listener and on the pre serialize event, checked to see if the object was soft-deleteable. if it was, then I disabled the soft deletable filter. In post serialization, I make sure the filter is turned back on.
This only works for lazy-loaded/proxy relationships. if your relationship fetch is set to EAGER, this will NOT work
Subscriber class:
<?php
namespace AppBundle\Event\Subscriber;
use Gedmo\SoftDeleteable\SoftDeleteable;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface as JmsEventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class JsonSerializerSubscriber implements JmsEventSubscriberInterface, EventSubscriberInterface
{
/**
* #var ContainerInterface
*/
protected $container;
public static function getSubscribedEvents()
{
return [
[
'event' => 'serializer.pre_serialize',
'method' => 'onPreSerialize',
],
[
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
],
];
}
public function onPreSerialize(ObjectEvent $objectEvent)
{
// before serializing; turn off soft-deleteable
$object = $objectEvent->getObject();
if ($object instanceof SoftDeleteable) {
$em = $this->container->get('doctrine.orm.default_entity_manager');
if ($em->getFilters()->isEnabled('softdeleteable')) {
$em->getFilters()->disable('softdeleteable');
}
}
}
public function onPostSerialize(ObjectEvent $objectEvent)
{
// after serializing; make sure that softdeletable filter is turned back on
$em = $this->container->get('doctrine.orm.default_entity_manager');
if (!$em->getFilters()->isEnabled('softdeleteable')) {
$em->getFilters()->enable('softdeleteable');
}
}
/**
* #param ContainerInterface $container
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
}
services.xml:
<services>
...
<service id="my.serializer.event_subscriber" class="AppBundle\Event\Subscriber\JsonSerializerSubscriber">
<call method="setContainer">
<argument type="service" id="service_container"/>
</call>
<tag name="jms_serializer.event_subscriber" />
</service>
...
</services>
Entity class (using op's example):
class SubCourses implements \Gedmo\SoftDeleteable\SoftDeleteable
{
...
}
Related
I'm using api platform in symfony (4) and without using a transformer (or rather: without using the output property) I'm getting the correct result.
However as I need to transform a logo (add a path) I need to integrate a transformer. As a result the response is empty.
ApiResource definition in Entity:
/**
*
* #ApiResource(
* collectionOperations = {
* "get"
* },
* normalizationContext={"groups" = {"frontend:read"}},
* itemOperations={
"get"
* },
* order={"name"="ASC"},
* paginationEnabled=false,
* output=EntityApiOutput::class
* )
*/
EntityApiOutput:
class EntityApiOutput
{
public $id;
}
EntityApiOutputDataTransformer:
class EntityApiOutputDataTransformer implements DataTransformerInterface
{
/**
* {#inheritdoc}
*/
public function transform($object, string $to, array $context = [])
{
$eao = new EntityApiOutput();
$eao->id = 3;
return $eao;
}
public function supportsTransformation($data, string $to, array $context = []): bool
{
return EntityApiOutput::class === $to && $data instanceof Entity;
}
}
entry in services.yaml:
App\DataTransformer\EntityApiOutputDataTransformer:
tags:
- { name: api_platform.data_transformer }
I simplified the transformer for reading purposes.
Putting a
dump($eao)
exit;
into the transform method confirms that the transformer is called and the EntityApiOutput object is filled.
Mhm unfortunately the api platform doc forgets to mention to also put the group into the output class:
class EntityApiOutput
{
/*
*
* #Groups({"frontend:read"})
*/
public $id;
}
That's how it should look like.
When implementing a rest json api with Symfony, one can deserialize the data for a create route with Jms Serializer:
$user = $serializer->deserialize($data, 'AppBundle\Entity\User', 'json');
but this makes all parameters of the User Entity available to set from the POST request, which might not be that good.
An alternative to this is to use setters in the controller:
$user = new User();
$user->setUsername($request->request->get('username'));
$user->sePassword($request->request->get('password'));
...
The latter option makes it more clear which parameters are actually able to set, but it requires a lot of code for a large entity.
What is the preferred way here?
Is it a third option?
You can serialize json data from your controller natively in Symfony once you have the Serializer component installed.
$user = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json');
When your object is created via this method, using the json from your request (decoded and then denormalized), the setters of your object are utilized to populate the properties of your object.
Could you post your User Entity?
Alternatively you can use Form Classes to perform this task.
Modification in relation to the comment on your question.
Annotation Groups in your entities works for serialization and deserialization.
class Item
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"first", "second"})
*/
private $id;
/**
* #ORM\Column(type="string", name="name", length=100)
* #Groups({"first"})
*/
private $name;
/**
* #ORM\Column(type="string", name="name", length=200)
* #Groups({"second"})
*/
private $description;
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
}
If you had both "name" and "description" in your POST data, you could insert either into your entity with the following:
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['first']]);
Or
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['second']]);
In the first case, only the name property would be populated and only the description property in the second case.
I'm trying to create a ManyToMany relation beetwin services of a company.
Each service had N parents services and N children services.
I looked at the doctrine documentation here : Many-To-Many, Self-referencing and I implemented it as followed :
Here is my service entity :
<?
namespace AppBundle\Entity;
class Service
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", inversedBy="parents")
* #ORM\JoinTable(name="app_services_hierarchy",
* joinColumns={#ORM\JoinColumn(name="parent_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="enfant_id", referencedColumnName="id")}
* )
*/
private $enfants;
public function __construct()
{
$this->enfants = new ArrayCollection();
$this->parents = new ArrayCollection();
}
public function getId(){
return $this->id;
}
//--------------------------------------------------Enfants
public function getEnfants(){
return $this->enfants;
}
public function setEnfants($enfant){
$this->enfants = $enfant;
}
public function addEnfant(Service $s){
$this->enfants[] = $s;
return $this;
}
public function removeEnfant(Service $s){
$this->enfants->removeElement($s);
}
//--------------------------------------------------Parents
public function getParents(){
return $this->parents;
}
public function setParents($parents){
$this->parents = $parents;
}
public function addParent(Service $s){
$this->parents[] = $s;
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
}
}
And here is my edit function( Controller.php) :
public function editAction(Request $request, $id)
{
$service = $this->getDoctrine()->getRepository(Service::class)->find($id);
$form = $this->createForm(ServiceType::class, $service);
$form ->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager ->persist($service);
dump($service);
$entityManager ->flush();
}
return $this->render('AppBundle:Service:edit.html.twig', array(
'form' => $form->createView(),
));
}
And the generated form looks like :
PROBLEM :
My problem is that the childrens are updated but not the parents. I can see the parents in the $service variable when I dump() it in my controler but the only ones updated in my database table (app_services_hierarchie) are the children.
The difference between $parents and $enfants in your code is that the service you are looking at is the Owning side in case of your $enfants mapping, but not in the case of your $parents mapping.
Doctrine will not store the $parents unless you tell it to do so via cascade={"persist"}.
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
This is basically the same anwer given in the post linked by #GregoireDucharme.
Edit: after some research, apparently this problem cannot be solved using cascade. According to the Doctrine documentation:
Doctrine will only check the owning side of an association for changes.
So what you have to do is tell your $parents to also update the $children property.
public function addParent(Service $s){
$this->parents[] = $s;
$s->addEnfant($this);
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
$s->removeEnfant($this);
}
In your form, make sure to specify the following:
->add('parents', 'collection', array(
'by_reference' => false,
//...
))
(I haven't spellchecked any of the code above, so tread carefully.)
If 'by_reference' is set to true, addParent and removeParent will not be called.
Credit goes to this blog post by Anny Filina.
It also states that you can remove the cascade option from your $parents property, but you probably should add cascade={"persist","remove"} to your $enfants property.
To use different Entity Manager / Connection based on URL in Symfony if fairly easy. With the following routing configuration
connection:
pattern: /a/{connection}
defaults: { _controller: AcmeTestBundle:User:index }
and from the following Cookbook;
How to work with Multiple Entity Managers and Connections
My controller would look something like this;
class UserController extends Controller
{
public function indexAction($connection)
{
$products = $this->get('doctrine')
->getRepository('AcmeStoreBundle:Product', $connection)
->findAll()
;
..................
and I'll be able to fetch product information from different em/connection/database.
Now, if I add something like this to my routing;
login:
pattern: /a/{connection}/login
defaults: { _controller: FOSUserBundle:Security:login }
How can I easily make the login to use connection as defined in the connection variable?
This setup assume each database has their own user login information (the fos_user table).
Edit: Updated routing information
Edit2:
I'm still new with PHP/Symfony/Doctrine though, so please forgive me if I'm completely wrong here. I tried to manually set the connection at FOS\UserBundle\Doctrine\UserManager. The following is the constructor of the class
//
use Doctrine\Common\Persistence\ObjectManager;
//
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer, CanonicalizerInterface $emailCanonicalizer, ObjectManager $om, $class)
{
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer);
$this->objectManager = $om;
$this->repository = $om->getRepository($class);
$metadata = $om->getClassMetadata($class);
$this->class = $metadata->getName();
}
In a controller, we can use the following method to change the em to 'testing'
$em = $this->get('doctrine')->getManager('testing');
$repository = $this->get('doctrine')->getRepository($class, 'testing')
For that I changed the code to the following to use EntityManager instead of ObjectManager.
//
//use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
//
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer, CanonicalizerInterface $emailCanonicalizer, EntityManager $om, $class)
{
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer);
$this->objectManager = $om;
$this->repository = $om->getRepository($class);
$metadata = $om->getClassMetadata($class);
$this->class = $metadata->getName();
}
My app works fine with no error.
From the way it works with the controller, I tried changing the connection by adding a parameter to this line then, but it's still using the default connection.
$this->repository = $om->getRepository($class, 'testing');
What else could I be missing here?
As you can see, FOSUserBundle can have only one EntityManager. You can see it from the settings orm.xml
<service id="fos_user.entity_manager" factory-service="doctrine" factory-method="getManager" class="Doctrine\ORM\EntityManager" public="false">
<argument>%fos_user.model_manager_name%</argument>
</service>
Parameter %fos_user.model_manager_name% specified in settings as model_manager_name
fos_user:
db_driver: ~ # Required
user_class: ~ # Required
firewall_name: ~ # Required
model_manager_name: ~
So into the constructor comes the instance of EntityManager, which does not accept the second parameter in the getRepository. Therefore, the standard FOSUserBundle can only work with one database.
But this is not the end of story, it's Symfony :)
We can write out UserManager, that can use different db connections. In the setting see that fos_user.user_manager is a fos_user.user_manager.default. We find it in orm.xml
<service id="fos_user.user_manager.default" class="FOS\UserBundle\Doctrine\UserManager" public="false">
<argument type="service" id="security.encoder_factory" />
<argument type="service" id="fos_user.util.username_canonicalizer" />
<argument type="service" id="fos_user.util.email_canonicalizer" />
<argument type="service" id="fos_user.entity_manager" />
<argument>%fos_user.model.user.class%</argument>
</service>
We can override this class to add an additional parameter that will determine what kind of connection you want to use. Further by ManagerFactory you can get the desired ObjectManager. I wrote simple example for the two databeses (if you need more databases you can write your factory for this service)
define your services in services.yml
services:
acme.user_manager.conn1:
class: Acme\DemoBundle\Service\UserManager
public: true
arguments:
- #security.encoder_factory
- #fos_user.util.username_canonicalizer
- #fos_user.util.email_canonicalizer
- #doctrine
- 'conn1_manager'
- %fos_user.model.user.class%
acme.user_manager.conn2:
class: Acme\DemoBundle\Service\UserManager
public: true
arguments:
- #security.encoder_factory
- #fos_user.util.username_canonicalizer
- #fos_user.util.email_canonicalizer
- #doctrine
- 'conn2_manager'
- %fos_user.model.user.class%
Your manager
/**
* Constructor.
*
* #param EncoderFactoryInterface $encoderFactory
* #param CanonicalizerInterface $usernameCanonicalizer
* #param CanonicalizerInterface $emailCanonicalizer
* #param RegistryInterface $doctrine
* #param string $connName
* #param string $class
*/
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer,
CanonicalizerInterface $emailCanonicalizer, RegistryInterface $doctrine, $connName, $class)
{
$om = $doctrine->getEntityManager($connName);
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer, $om, $class);
}
/**
* Just for test
* #return EntityManager
*/
public function getOM()
{
return $this->objectManager;
}
and simple test
/**
* phpunit -c app/ src/Acme/DemoBundle/Tests/FOSUser/FOSUserMultiConnection.php
*/
class FOSUserMultiConnection extends WebTestCase
{
public function test1()
{
$client = static::createClient();
/** #var $user_manager_conn1 UserManager */
$user_manager_conn1 = $client->getContainer()->get('acme.user_manager.conn1');
/** #var $user_manager_conn2 UserManager */
$user_manager_conn2 = $client->getContainer()->get('acme.user_manager.conn2');
/** #var $om1 EntityManager */
$om1 = $user_manager_conn1->getOM();
/** #var $om2 EntityManager */
$om2 = $user_manager_conn2->getOM();
$this->assertNotEquals($om1->getConnection()->getDatabase(), $om2->getConnection()->getDatabase());
}
}
I'm sorry that the answer was so big. If something is not clear to the end, I put the code on github
FosUserBundle is not able to have more than one entity manager.
The easiest way I found to use 2 databases, is to override the 'checkLoginAction' of the SecurityController.
<?php
//in myuserBunle/Controller/SecurityController.php
class SecurityController extends BaseController
{
/**
* check the user information
*/
public function checkLoginAction(Request $request){
$username = \trim($request->request->get("_username"));
$user = $this->container->get('fos_user.user_manager')->findUserByUsername($username);
$userDB2 = .....
$password = \trim($request->request->get('_password'));
if ($user) {
// Get the encoder for the users password
$encoder = $this->container->get('security.encoder_factory')->getEncoder($user);
$encoded_pass = $encoder->encodePassword($password, $user->getSalt());
if (($user->getPassword() == $encoded_pass) || $this->checkSecondEM()) {
$this->logUser($request, $user);
return new RedirectResponse($this->container->get('router')->generate($this->container->get('session')->get('route'), $request->query->all() ));
} else {
// Password bad
return parent::loginAction($request);
}
} else {
// Username bad
return parent::loginAction($request);
}
}
}
Would be possible to have a custom repository not associated with an entity in Symfony 2 and Doctrine 2? I would like to put in it some native SQL that doesn't fit well in other repositories (it may refer to abstract or entity hierarchy).
How controller code $this->getDoctrine()->getRepositoty(/* ??? */) should be replaced?
It's possible to have as many repositories as you wish. However, only a single repository can be linked with the entity manager.
You need to define a few services to add a custom repository.
<!-- My custom repository -->
<service id="acme.repository.my_entity" class="Acme\FQCN\MyEntityRepository" >
<argument type="service" id="doctrine.orm.entity_manager" />
<argument type="service" id="acme.metadata.my_entity" />
</service>
<!-- MyEntity metadata -->
<service id="acme.metadata.my_entity" class="Doctrine\ORM\Mapping\ClassMetaData">
<argument>Acme\FQCN\MyEntity</argument>
</service>
The repository class would have to inherit from EntityRepository.
namespace Acme\FQCN;
use Doctrine\ORM\EntityRepository;
class MyEntityRepository extends EntityRepository
{
/**
* If you want to inject any custom dependencies, you'd have either have to
* add them to the construct or create setters. I'd suggest using setters
* in which case you wouldn't need to use the constructor in this class.
*
* public function __construct($em, Doctrine\ORM\Mapping\ClassMetadata $class, $custom_dependency)
* {
* parent::__construct($em, $class);
* }
*
*/
}
Unfortunately you'll not be able to retrieve it via the doctrine service. Instead, retrieve it straight from the container:
$this->get('acme.repository.my_entity');
EDIT
If you're creating a repository that shouldn't be linked to any entities, simply create a service and inject the necessary dependencies.
<!-- Repository for misc queries -->
<service id="acme.repository.misc" class="Acme\FQCN\MiscRepsitory">
<argument type="service" id="database_connection" />
</service>
Since you're not using any of the Doctrine's ORM features in a custom repository, there's no need to extend EntityManager.
namespace Acme\FQCN;
use \Doctrine\DBAL\Connection;
class MiscRepository
{
protected $conn;
public function __construct(Connection $conn)
{
$this->conn = $conn;
}
}
I adopted a slightly different solution using Symfony2 parent services.
First of all I created a parent service, a GenericRepository class that exposes a couple of methods and makes life easier in case we'd like to refactor our code in the future.
services.yml
acme_core.generic_repository:
abstract: true
class: Acme\Bundle\CoreBundle\Repository\GenericRepository
arguments: [#doctrine.orm.entity_manager]
Acme\Bundle\CoreBundle\Repository\GenericRepository
<?php
namespace Acme\Bundle\CoreBundle\Repository;
use Doctrine\ORM\EntityManager;
/**
* Class GenericRepository
* #package Acme\Bundle\CoreBundle\Repository
*/
abstract class GenericRepository {
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager) {
$this->entityManager = $entityManager;
}
/**
* #return EntityManager
*/
public function getEntityManager() {
return $this->entityManager;
}
/**
* #return \Doctrine\DBAL\Connection
*/
public function getConnection() {
return $this->getEntityManager()->getConnection();
}
/**
* #return string
*/
abstract function getTable();
}
Now we want to define a new repository:
services.yml
# Repositories
acme_product.repository.product_batch:
parent: acme_core.generic_repository
class: Acme\Bundle\ProductBundle\Repository\ProductBatchRepository
Acme\Bundle\ProductBundle\Repository\ProductBatchRepository
<?php
namespace Acme\Bundle\ProductBundle\Repository;
use Acme\Bundle\CoreBundle\Repository\GenericRepository;
/**
* Class ProductBatchRepository
* #package Acme\Bundle\ProductBundle\Repository
*/
class ProductBatchRepository extends GenericRepository {
/**
* #param int $batchId
* #return integer The number of affected rows.
*/
public function deleteBatch($batchId) {
$table = $this->getTable();
return $this->getConnection()->delete($table, [
'id' => $batchId
]);
}
/**
* {#inheritdoc}
*/
public function getTable() {
return 'product_batch';
}
}
The deleteBatch() method creates and executes the following query:
DELETE FROM product_batch WHERE id = ?
Finally in our controller:
public function deleteAction() {
$batchId = $this->getRequest()->get('batchId');
$affectedRows = $this->get('acme_product.repository.product_batch')->deleteBatch($batchId);
return $this->render(/**/);
}
For further information and entity manager / connection usage please refer to the official documentation: http://doctrine-orm.readthedocs.org/en/latest/reference/native-sql.html
My suggestion is to create a plain PHP class with the needed dependencies in the constructor and get it through the service container.