Symfony3 Related Entity not mapped back to parent on creation - symfony

I have 2 entities Client and Address with a OneToMany relationship.
When I create a new client with an address, it saves both the client and address but does not set the client_id on the address, it is NULL.
I am using fosrestbundle and jmsserializer and sending the data as a json object.
In my controller I have the following:
/**
* #REST\POST("/clients", name="create_client")
*/
public function createClientAction(Request $request)
{
$serializer = $this->get('jms_serializer');
$client = $serializer->deserialize($request->getContent(), 'AppBundle\Entity\Client', 'json');
$em = $this->getDoctrine()->getManager();
$em->persist($client);
$em->flush();
return $this->view($client, 200);
}
Here is a simplified request payload:
{ "name" : "foo", "addresses" : [ { "zip" : "12345" } ]
In my database it creates a new client with name = foo along with an address with zip = 12345, however the field client_id is NULL
My entities are mapped as follows:
//Client.php
...
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="client", cascade={"persist", "remove"})
*/
private $addresses;
//Address.php
...
...
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="addresses")
*
*/
private $client;
update
I'm even more confused now, I just realized I do not have any getters/setters in my entities, yet I am able to get / set data.
I am guessing setting has something to do with serializer->deserialize. I have the following in my services:
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false
And getting has something to do with the fosrestbundle. Here is the get route:
/**
* #REST\GET("/clients/{client}", name="get_client")
*/
public function getClientAction(Request $request, Client $client)
{
return $this->view($client, 200);
}

Apparently I needed to merge and not persist the entity.
I updated the route:
$em = $this->getDoctrine()->getManager();
$em->merge($client);
$em->flush();
and the Client entity:
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="client", cascade={"persist", "remove", "merge"})
*/
private $addresses;
Seems to be working now!

Related

How to execute a Symfony command in background from a controller

I have a command which take a long time to run (it generates a big file).
I would like to use a controller to start it in background and don't wait for the end of its execution to render a view.
Is it possible? If yes, how?
I though the Process class would be useful but the documentation says:
If a Response is sent before a child process had a chance to complete, the server process will be killed (depending on your OS). It means that your task will be stopped right away. Running an asynchronous process is not the same as running a process that survives its parent process.
I solved my problem using the Messenger component as #msg suggested in comments.
To do so, I had to:
install the Messenger component by doing composer require symfony/messenger
create a custom log entity to track the file generation
create a custom Message and a custom MessageHandler for my file generation
dispatch the Message in my controller view
move my command code to a service method
call the service method in my MessageHandler
run bin/console messenger:consume -vv to handle the messages
Here is my code:
Custom log entity
I use it to show in my views if a file is being generated and to let the user download the file if its generation is complete
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\MyLogForTheBigFileRepository")
*/
class MyLogForTheBigFile
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $generationDateStart;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $generationDateEnd;
/**
* #ORM\Column(type="string", length=200, nullable=true)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $generator;
public function __construct() { }
// getters and setters for the attributes
// ...
// ...
}
Controller
I get the form submission and dispatch a message which will run the file generation
/**
* #return views
* #param Request $request The request.
* #Route("/generate/big-file", name="generate_big_file")
*/
public function generateBigFileAction(
Request $request,
MessageBusInterface $messageBus,
MyFileService $myFileService
)
{
// Entity manager
$em = $this->getDoctrine()->getManager();
// Creating an empty Form Data Object
$myFormOptionsFDO = new MyFormOptionsFDO();
// Form creation
$myForm = $this->createForm(
MyFormType::class,
$myFormOptionsFDO
);
$myForm->handleRequest($request);
// Submit
if ($myForm->isSubmitted() && $myForm->isValid())
{
$myOption = $myFormOptionsFDO->getOption();
// Creating the database log using a custom entity
$myFileGenerationDate = new \DateTime();
$myLogForTheBigFile = new MyLogForTheBigFile();
$myLogForTheBigFile->setGenerationDateStart($myFileGenerationDate);
$myLogForTheBigFile->setGenerator($this->getUser());
$myLogForTheBigFile->setOption($myOption);
// Save that the file is being generated using the custom entity
$em->persist($myLogForTheBigFile);
$em->flush();
$messageBus->dispatch(
new GenerateBigFileMessage(
$myLogForTheBigFile->getId(),
$this->getUser()->getId()
));
$this->addFlash(
'success', 'Big file generation started...'
);
return $this->redirectToRoute('bigfiles_list');
}
return $this->render('Files/generate-big-file.html.twig', [
'form' => $myForm->createView(),
]);
}
Message
Used to pass data to the service
namespace App\Message;
class GenerateBigFileMessage
{
private $myLogForTheBigFileId;
private $userId;
public function __construct(int $myLogForTheBigFileId, int $userId)
{
$this->myLogForTheBigFileId = $myLogForTheBigFileId;
$this->userId = $userId;
}
public function getMyLogForTheBigFileId(): int
{
return $this->myLogForTheBigFileId;
}
public function getUserId(): int
{
return $this->userId;
}
}
Message handler
Handle the message and run the service
namespace App\MessageHandler;
use App\Service\MyFileService;
use App\Message\GenerateBigFileMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class GenerateBigFileMessageHandler implements MessageHandlerInterface
{
private $myFileService;
public function __construct(MyFileService $myFileService)
{
$this->myFileService = $myFileService;
}
public function __invoke(GenerateBigFileMessage $generateBigFileMessage)
{
$myLogForTheBigFileId = $generateBigFileMessage->getMyLogForTheBigFileId();
$userId = $generateBigFileMessage->getUserId();
$this->myFileService->generateBigFile($myLogForTheBigFileId, $userId);
}
}
Service
Generate the big file and update the logger
public function generateBigFile($myLogForTheBigFileId, $userId)
{
// Get the user asking for the generation
$user = $this->em->getRepository(User::class)->find($userId);
// Get the log object corresponding to this generation
$myLogForTheBigFile = $this->em->getRepository(MyLogForTheBigFile::class)->find($myLogForTheBigFileId);
$myOption = $myLogForTheBigFile->getOption();
// Generate the file
$fullFilename = 'my_file.pdf';
// ...
// ...
// Update the log
$myLogForTheBigFile->setGenerationDateEnd(new \DateTime());
$myLogForTheBigFile->setFilename($fullFilename);
$this->em->persist($myLogForTheBigFile);
$this->em->flush();
}

serialization with jms in symfony

I have a User entity that contains address. I will save address as a json in my database. After form validation, I have to manually serialize address before persisting data. Is there anyway to avoid doing it like that ? Is it possible to call serialize event when doctrine is persisting data ?
class User{
/**
* #ORM\Column(name="username", type="string", length=30)
**/
private $username;
/**
* #ORM\Column(name="address", type="json")
**/
private $address;
}
class Address{
private $postalcode;
private $street;
}
// Inside my controller
class UserController extends Controller{
/**
* #Rest\View(StatusCode = Response::HTTP_CREATED)
*
* #Rest\Post(
* path = "/user",
* name = "user_create"
* )
*/
public function createAction(){
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->submit($request->request->all());
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$user->setAddress($this->get('jms_serializer')->serialize($user->getAddress(), 'json'));
$em->persist($user);
$em->flush();
return $this->view($user, Response::HTTP_CREATED);
}
return $form;
}
}
Doctrine failed to make it json because address property was private (PHP json_encode returning empty sctructure). Making it public resolve the problem.
When defining type as json, doctrine will use php's json encoding functions: https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#json-array
Thanks

Use deserialize or getters and setters in controller actions

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.

How to use the registration procedure of FOSUserBundle from a third controller

I have to persist an entity (let's call it Entity for simplicity) in the database that has to be referenced to a User handled with FOSUserBundle. To make this reference I have a column entity_table.userId.
When the new Entity is created, I have to:
Create the User through the registration procedure of FosUserBundle;
Get the ID of the new created User: [meta code] $userId = $get->newCreatedUserId();
Set this id in Entity: $entity->setUserId($userId);
Persist the Entity to the database.
How can I integrate the registration procedure of FosUserBundle into the controller that persists my Entity?
MORE DETAILS
In the first time I tried to simply copy the code from the method registerAction() of the RegistrationController of FOSUserBundle: a quick and dirty approach that, anyway didn't work as i get an error as the User class i passed was wrong (I passed my custom User entity I use to overwrite the bundle).
This kind of approach has also other drawbacks:
I cannot control the registration procedure (send or decide to not send confirmation e-mails, for example);
I cannot use the builtin checks on passed data;
I cannot be sure that on FOSUserBundles updates my custom method continue to work
Others I cannot imagine at the moment...
So, I'd like to create the user in the cleanest way possible: how can i do this? Which should be a good approach?
A controller forwarding?
Anyway, an "hardcoded" custom method that emulates the registerAction() method?
A custom registration form?
I have read a lot of discussions here at StackOverflow and on Internet, I read the documentation of FOSUserBundle and of Symfony too, but I cannot decide for the good approach, also because I'm not sure I have understood all the pros and cons of each method.
If someone can put me on the right way... :)
SOMETHING MORE ABOUT MY REGISTRATION FLOW
I have a getStarted procedure handled by the controller GetStarteController.
In it I have two methods:
indexAction(), that displays a registration form with only the field "email";
endAction(), that receive the form and creates a Company using the passed e-mail (it gets the domain part only of the email).
HERE IS A WORKING MESSY CODE (inside it for Companies and Stores are called some methods that exists in the source code but are not in the posted classes below, as setBrand() or setUrl(), for example).
// AppBundle/Controller/getStartedController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use MyVendor\UserBundle\Entity\User;
use AppBundle\Entity\Companies;
use AppBundle\Entity\Stores;
class GetStartedController extends Controller
{
/**
* #Route("getstarted")
* #Template()
*/
public function indexAction()
{
$data = array();
$form = $this->createFormBuilder($data, array(
'action' => $this->generateUrl('getStartedEnd'),
))
->add('email', 'email')
->add('submit', 'submit')
->getForm();
return array(
'form' => $form->createView(),
);
}
/**
* #Route("getstarted/end", name="getStartedEnd")
* #Template()
*/
public function endAction(Request $request)
{
$form = $this->createFormBuilder()
->add('email', 'email')
->add('submit', 'submit')
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData();
} else {
/** #todo here we have to raise some sort of exception or error */
echo 'no data submitted (See the todo in the code)';exit;
}
// Pass the email to the template
$return['email'] = $data['email'];
// Get the domain part of the email and pass it to the template
$domain = explode('#', $data['email']);
$return['domain'] = $domain[1];
// 1) Create the new user
$user = new User();
// Get the token generator
$tokenGenerator = $this->container->get('fos_user.util.token_generator');
$user->setEmail($return['email']);
$userRandomUsername = substr($tokenGenerator->generateToken(), 0, 12);
$user->setUsername('random-' . $userRandomUsername);
$plainPassword = substr($tokenGenerator->generateToken(), 0, 12);
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);
// Set the password for the user
$user->setPassword($encoded);
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
// Perstist the user in the database
$userManager->updateUser($user);
$userId = $user->getId();
// 2) Create the Company object
$company = new Companies();
$company->setBrand($return['domain'])
->setAdded(new \DateTime())
->setOwnerId($userId);
// 3) Create the Store object
$store = new Stores();
$store->setEmail($return['email'])
->setUrl($return['domain'])
->setAdded(new \DateTime());
// Get the Entity Manager
$em = $this->getDoctrine()->getManager();
// Persist Company and get its ID
$em->persist($company);
$em->flush();
$return['companyId'] = $company->getId();
// Set the property branchOf of the Store object
$store->setBranchOf($return['companyId']);
// Persist the Store object
$em->persist($store);
$em->flush();
$return['storeId'] = $store->getId();
return $return;
}
}
Here the User Entity that ovewrites the one provided by FOSUserBundle
// MyVendor/UserBundle/Entity/User.php
namespace MyVendor\UserBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="prefix_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
}
Some essential code of Companies.php
// AppBundle/Entity/Companies.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Companies
*
* #ORM\Table(name="companies")
* #ORM\Entity
*/
class Companies
{
/**
* #var integer
*
* #ORM\Column(name="ownerId", type="integer", nullable=false)
*/
private $ownerid;
/**
* Set ownerid
*
* #param integer $ownerid
* #return Companies
*/
public function setOwnerid($ownerid)
{
$this->ownerid = $ownerid;
return $this;
}
/**
* Get ownerid
*
* #return integer
*/
public function getOwnerid()
{
return $this->ownerid;
}
}
Some essential code of Stores.php
// AppBundle/Entity/Stores.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Stores
*
* #ORM\Table(name="stores", uniqueConstraints={#ORM\UniqueConstraint(name="branchOf", columns={"branchOf"})})
* #ORM\Entity
*/
class Stores
{
/**
* #var integer
*
* #ORM\Column(name="branchOf", type="integer", nullable=false)
*/
private $branchof;
/**
* Set branchof
*
* #param integer $branchof
* #return Stores
*/
public function setBranchof($branchof)
{
$this->branchof = $branchof;
return $this;
}
/**
* Get branchof
*
* #return integer
*/
public function getBranchof()
{
return $this->branchof;
}
}
You can use a custom registration form but the best way is clearly to listen to registration event dispatched by FOSUser.
Here is an example :
class RegistrationListener implements EventSubscriberInterface
{
/**
* L'entity manager
*
* #var EntityManager
*/
private $em;
/**
* Constructeur de l'EventListener
*
* #param \Doctrine\ORM\EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit',
);
}
/**
* Triggered when FOSUserEvents::REGISTRATION_INITIALIZE is caught.
*
* #param \FOS\UserBundle\Event\UserEvent $userEvent
*/
public function onRegistrationInit(UserEvent $userEvent)
{
$user = $userEvent->getUser();
// Define your own logic there
}
}
Don't forget to make this listener a service:
#services.yml
services:
oe_user.registration:
class: OrienteExpress\UserBundle\EventListener\RegistrationListener
# arguments are optional but you still can need them
# so I let the EM as example which is an often used parameter
arguments:
entityManager: "#doctrine.orm.entity_manager"
tags:
- { name: kernel.event_subscriber }
You'll find the complete list of event dispatched by FOSUser here
Moreover, Symfony entities are a model of objects. That said, you need to understand that you don't work with ids within your model, but object.
You should not use thing such as $var->setUserId() within entites. Doctrine is there to manage your relations, so be carefull about this. You might face unexpected problem by not using Symfony & Doctrine the way it has been designed for.
EDIT:
In your company entity, your relation is beetween a Company and a User objects. That means you dont need a User id in your company but just a instance of User.
I think you might go back to the basics before wanting to do advanced stuff.
Your relation beetween the user and the company should not be designed by an integer attribute but a real doctrine relation.
Ex:
class Company {
/**
* #ORM\ManyToOne(targetEntity="Path\To\User")
* #ORM\JoinColumn(nullable=false)
*/
private $owner;
/**
* #param $user User
*/
public function setUser(User $user)
{
$this->user = $user;
}
}
Then when you'll create a new company. You won't need to know the User's id or even insert it to make the link between them. But if you are not aware yet of this, once again, I think you should go back to the basics of Symfony since this is one of the most (maybe the most) important feature to master.

Symfony - Avoid store related entities in preFlush Doctrine

I have an entity Subject:
/**
*
* #ORM\Table()
* #ORM\HasLifecycleCallbacks()
*/
class Subject
{
//... Some fields
/**
* #ORM\OneToMany(targetEntity="Subject", mappedBy="mark", cascade={"persist", "remove"})
*/
private $subjects;
private function calculateMarks()
{
//... Do something
// return array with (abilities => marks);
}
/**
* #ORM\PrePersist()
*/
public function prePersist(){
$now = new \DateTime();
$this->setCreatedAt( $now );
$this->setModifiedAt( $now );
}
/**
* #ORM\PreUpdate()
*/
public function preUpdate(){
$this->setModifiedAt( new \DateTime() );
$this->setUpdated(true);
}
/**
* #ORM\PreFlush()
*/
public function preFlush(){
$marks = calculateMarks();
foreach($marks as $ability => $score){
$mark = new Mark();
$mark->setSubject( $this );
$this->addMark( $score );
$mark->setAbility( $ability );
}
}
}
and the class Mark:
class Mark{
// Many fields
/**
* #ORM\ManyToOne(targetEntity="Subject", inversedBy="subjects")
* #ORM\JoinColumn(name="subject_id", referencedColumnName="id")
*/
private $subject;
}
My problem is that I calculate and I create the Marks in the preFlush event (this is done this because in the official documentation is said this about preUpdate event: "Changes to associations of the updated entity are never allowed in this event, since Doctrine cannot guarantee to correctly handle referential integrity at this point of the flush operation"). When I save one subject, all work fine, but when I save many Subjects at the same time in a webservice, some marks are stored in the database many times.
The webservice action below:
public function setSubjects(Request $request)
{
//... Do something
$subjects = $request["Subjects"];
foreach($subjects as $s){
$em = $this->getDoctrine()->getManager();
//... Do something
$em->persist($s);
$em->flush();
}
return new JsonResponse($response);
}
Has anybody an idea of how could I avoid this behavior in the preFlush event?
Thanks in advance.
I always try to avoid LifecycleCallbacks unless it's simple and i'm only changing properties in the same entity.
to solve your issue i would create a function calculateMarks() inside the entity and tweak my loop to be something like
$em = $this->getDoctrine()->getManager();
foreach($subjects as $s){
//... Do something
$s->calculateMarks();
$em->persist($s);
}
$em->flush();
NOTICE
avoid $em = $this->getDoctrine()->getManager(); & $em->flush(); inside the loop

Resources