I'm simply attempting to augment an array in Symfony like thus;
$inputters = $project->getInputters();
foreach($inputters as $in){
$ab[] = $in->getName();
}
$project is a Symfony entity. It turns out when I var dump $ab[] I actually get two separate arrays? Like thus:
array(1) {
[0]=>
string(9) "Bob Right"
}
array(1) {
[0]=>
string(14) "Sally Brown"
}
for example. But what I'm really after is a single array, like:
array(2) {
[0]=>
string(9) "Bob Right",
[1]=>
string(11) "Sally Brown"
}
for instance. The $project->getInputters() method returns a collection. Like:
Project
/**
* #ORM\ManyToMany(targetEntity="App\Entity\User", inversedBy="inputterProjects")
* #ORM\JoinTable(name="project_inputters",
* joinColumns={#ORM\JoinColumn(name="project_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")}
* )
* #var ArrayCollection<int,User>
*/
private $inputters;
/**
* #return Collection
*/
public function getInputters(): Collection
{
return $this->inputters;
}
Why is this simple array augmentation producing two separate arrays when I'm expecting only one?
Help.
edits;
foreach($inputters as $in){
$ab[] = is_array($in->getName());
}
array(1) {
[0]=>
bool(false)
}
array(1) {
[0]=>
bool(false)
}
edits1
foreach($inputters as $in){
$ab[] = get_class($in);
}
var_dump($ab);
array(1) { [0]=> string(15) "App\Entity\User" }
User
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
My loop was inside an array_map() construct. I think this was the issue. Knocking it out of whack.
Related
It's my first Symfony app.
I try to do an intervention manager, so I have created three entities: Intervention, Comment and User.
Users are managed by FOSUserBundle.
Classes:
Intervention
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\InterventionRepository")
*/
class Intervention
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="smallint")
*/
private $num_rue;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom_rue;
/**
* #ORM\Column(type="string", length=5)
*/
private $cp;
/**
* #ORM\Column(type="decimal", precision=12, scale=9, nullable=true)
*/
private $lat;
/**
* #ORM\Column(type="decimal", precision=12, scale=9, nullable=true)
*/
private $longi;
/**
* #ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* #ORM\Column(type="boolean", options={"default":true})
*/
private $statut;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="intervention", orphanRemoval=true)
*/
private $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNumRue(): ?int
{
return $this->num_rue;
}
public function setNumRue(int $num_rue): self
{
$this->num_rue = $num_rue;
return $this;
}
public function getNomRue(): ?string
{
return $this->nom_rue;
}
public function setNomRue(string $nom_rue): self
{
$this->nom_rue = $nom_rue;
return $this;
}
public function getCp(): ?string
{
return $this->cp;
}
public function setCp(string $cp): self
{
$this->cp = $cp;
return $this;
}
public function getLat()
{
return $this->lat;
}
public function setLat($lat): self
{
$this->lat = $lat;
return $this;
}
public function getLongi()
{
return $this->longi;
}
public function setLongi($longi): self
{
$this->longi = $longi;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getStatut(): ?bool
{
return $this->statut;
}
public function setStatut(bool $statut): self
{
$this->statut = $statut;
return $this;
}
/**
* #return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setIntervention($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
// set the owning side to null (unless already changed)
if ($comment->getIntervention() === $this) {
$comment->setIntervention(null);
}
}
return $this;
}
}
Comment
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\CommentRepository")
*/
class Comment
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="text")
*/
private $text;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Intervention", inversedBy="comments")
* #ORM\JoinColumn(name="intervention_id", referencedColumnName="id")
*/
private $intervention;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="comments")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
public function getId(): ?int
{
return $this->id;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
public function getIntervention(): ?Intervention
{
return $this->intervention;
}
public function setIntervention(?Intervention $intervention): self
{
$this->intervention = $intervention;
return $this;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(?User $author): self
{
$this->author = $author;
return $this;
}
}
User
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="author")
*/
private $comments;
public function __construct()
{
parent::__construct();
$this->comments = new ArrayCollection();
}
/**
* #return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setAuthor($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
}
The problem is when I try to access the Intervention with Doctrine, I have a circular reference.
I thought that comments under intervention return user collection which also returns comments...
$repository = $this->getDoctrine()->getRepository(Intervention::class);
$intervention = $repository->findBy(['statut' => 1]);
return $this->json($intervention);
Do you have any suggestion to make my code work please?
Thank you.
Solution:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
public function getLocations(){
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object, string $format = null, array $context = array()) {
return $object->getId();
});
$serializer = new Serializer(array($normalizer), array($encoder));
$repository = $this->getDoctrine()->getRepository(Intervention::class);
$intervention = $repository->findBy(['statut' => 1]);
$response = new Response($serializer->serialize($intervention, 'json'));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
Thanks for your help.
this is just the 1/100 of the dump:
array(3) { [0]=> object(App\Entity\Intervention)#483 (9) { ["id":"App\Entity\Intervention":private]=> int(1) ["num_rue":"App\Entity\Intervention":private]=> int(4) ["nom_rue":"App\Entity\Intervention":private]=> string(14) "rue d’alsace" ["cp":"App\Entity\Intervention":private]=> string(5) "49000" ["lat":"App\Entity\Intervention":private]=> string(12) "47.470484600" ["longi":"App\Entity\Intervention":private]=> string(12) "-0.551233000" ["description":"App\Entity\Intervention":private]=> string(69) "
étanchéité toiture
carrelage SB
" ["statut":"App\Entity\Intervention":private]=> bool(true) ["comments":"App\Entity\Intervention":private]=> object(Doctrine\ORM\PersistentCollection)#485 (9) { ["snapshot":"Doctrine\ORM\PersistentCollection":private]=> array(0) { } ["owner":"Doctrine\ORM\PersistentCollection":private]=> *RECURSION* ["association":"Doctrine\ORM\PersistentCollection":private]=> array(15) { ["fieldName"]=> string(8) "comments" ["mappedBy"]=> string(12) "intervention" ["targetEntity"]=> string(18) "App\Entity\Comment" ["cascade"]=> array(0) { } ["orphanRemoval"]=> bool(true) ["fetch"]=> int(2) ["type"]=> int(4) ["inversedBy"]=> NULL ["isOwningSide"]=> bool(false) ["sourceEntity"]=> string(23) "App\Entity\Intervention" ["isCascadeRemove"]=> bool(true) ["isCascadePersist"]=> bool(false) ["isCascadeRefresh"]=> bool(false) ["isCascadeMerge"]=> bool(false) ["isCascadeDetach"]=> bool(false) } ["em":"Doctrine\ORM\PersistentCollection":private]=> object(Doctrine\ORM\EntityManager)#234 (11) { ["config":"Doctrine\ORM\EntityManager":private]=> object(Doctrine\ORM\Configuration)#188 (1) { ["_attributes":protected]=> array(14) { ["entityNamespaces"]=> array(1) { ["App"]=> string(10) "App\Entity" } ["metadataCacheImpl"]=> object(Symfony\Component\Cache\DoctrineProvider)#195 (3) { ["pool":"Symfony\Component\Cache\DoctrineProvider":private]=> object(Symfony\Component\Cache\Adapter\PhpFilesAdapter)#197 (16) { ["createCacheItem":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> object(Closure)#199 (2) { ["static"]=> array(1) { ["defaultLifetime"]=> int(0) } ["parameter"]=> array(3) { ["$key"]=> string(10) "" ["$value"]=> string(10) "" ["$isHit"]=> string(10) "" } } ["mergeByLifetime":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> object(Closure)#201 (2) { ["static"]=> array(1) { ["getId"]=> object(Closure)#198 (2) { ["this"]=> *RECURSION* ["parameter"]=> array(1) { ["$key"]=> string(10) "" } } } ["parameter"]=> array(3) { ["$deferred"]=> string(10) "" ["$namespace"]=> string(10) "" ["&$expiredIds"]=> string(10) "" } } ["namespace":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> string(0) "" ["namespaceVersion":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> string(0) "" ["versioningIsEnabled":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> bool(false) ["deferred":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> array(0) { } ["ids":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> array(4) { ["DoctrineNamespaceCacheKey%5B%5D"]=> string(31) "DoctrineNamespaceCacheKey%5B%5D" ["%5BApp%5CEntity%5CUser%24CLASSMETADATA%5D%5B1%5D"]=> string(48) "%5BApp%5CEntity%5CUser%24CLASSMETADATA%5D%5B1%5D" ["%5BApp%5CEntity%5CComment%24CLASSMETADATA%5D%5B1%5D"]=> string(51) "%5BApp%5CEntity%5CComment%24CLASSMETADATA%5D%5B1%5D" ["%5BApp%5CEntity%5CIntervention%24CLASSMETADATA%5D%5B1%5D"]=> string(56) "%5BApp%5CEntity%5CIntervention%24CLASSMETADATA%5D%5B1%5D" } ["maxIdLength":protected]=> NULL ["logger":protected]=> object(Symfony\Bridge\Monolog\Logger)#196 (5) { ["name":protected]=> string(5) "cache" ["handlers":protected]=> array(2) { [0]=> object(Monolog\Handler\FingersCrossedHandler)#94 (11) { ["handler":protected]=> object(Monolog\Handler\StreamHandler)#92 (10) { ["stream":protected]=> NULL ["url":protected]=> string(56) "xxxxxxxxxxxxxxxxxxxxxxxx/var/log/prod.log" ["errorMessage":"Monolog\Handler\StreamHandler":private]=> NULL ["filePermission":protected]=> NULL ["useLocking":protected]=> bool(false) ["dirCreated":"Monolog\Handler\StreamHandler":private]=> NULL ["level":protected]=> int(100) ["bubble":protected]=> bool(true) ["formatter":protected]=> NULL ["processors":protected]=> array(1) { [0]=> object(Monolog\Processor\PsrLogMessageProcessor)#93 (0) { } } } ["activationStrategy":protected]=> object(Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy)#95 (3) { ["blacklist":"Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy":private]=> string(7) "{(^/)}i" ["requestStack":"Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy":private]=> object(Symfony\Component\HttpFoundation\RequestStack)#96 (1) { ["requests":"Symfony\Component\HttpFoundation\RequestStack":private]=> array(1) { [0]=> object(Symfony\Component\HttpFoundation\Request)#4 (23) { ["attributes"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#7 (1) { ["parameters":protected]=> array(5) { ["_route"]=> string(13) "location_list" ["_controller"]=> string(44) "App\Controller\OsimgController::getLocations" ["_route_params"]=> array(0) { } ["_firewall_context"]=> string(34) "security.firewall.map.context.main" ["_security"]=> array(1) { [0]=> object(Sensio\Bundle\FrameworkExtraBundle\Configuration\Security)#407 (3) { ["expression":"Sensio\Bundle\FrameworkExtraBundle\Configuration\Security":private]=> string(21) "has_role('ROLE_USER')" ["statusCode":protected]=> NULL ["message":protected]=> string(14) "Access denied." } } } } ["request"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#5 (1) { ["parameters":protected]=> array(0) { } } ["query"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#6 (1) { ["parameters":protected]=> array(0) { } } ["server"]=> object(Symfony\Component\HttpFoundation\ServerBag)#10 (1) { ["parameters":protected]=> array(66) { ["PATH"]=> string(28) "/usr/local/bin:/usr/bin:/bin" ["TEMP"]=> string(4) "/tmp" ["TMP"]=> string(4) "/tmp" ["TMPDIR"]=> string(4) "/tmp" ["PWD"]=> string(1) "/" ["HTTP_ACCEPT"]=>
If you want to return as a json, serializer will serialize it,
You have 3 options,
You can Install jms serializer, which is more easy to serialize, less painless.
- https://symfony.com/doc/current/components/serializer.html#handling-circular-references
or
- https://symfony.com/doc/current/reference/configuration/framework.html#reference-serializer-circular-reference-handler
List item
I would like to deserialize a JSON to an object having an entity relation.
incoming JSON data
{
"name": "john",
"books": [
{
"title": "My life"
}
]
}
The result of json deserialization like this
$object = $this->get('serializer')->deserialize($jsonData, 'Author', 'json');
is
Author { #name: 'john' #books: array:1 [ 0 => array:1 [ "title" => "My life" ] ] }
I would like to deserialize to an object like this
Author { #name: 'john' #books: array:1 [ Book { "title" => "My life" } ] }
I understand why deserialization is not able to deserialize Book. With JMSSerialzerBundle, the Type annotation exists to resolve that case.
Is it possible to do it with the Symfony Serializer component or must i use the JMSSerializer for that ?
Thanks for your help ;)
My objects
class Author
{
private $name;
private $books;
/**
* #return mixed
*/
public function getName()
{
return $this->name;
}
/**
* #param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* #return mixed
*/
public function getBooks()
{
return $this->books;
}
/**
* #param mixed $books
*/
public function setBooks(array $books)
{
$this->books = $books;
}
}
class Book
{
private $title;
private $author;
/**
* #return mixed
*/
public function getTitle()
{
return $this->title;
}
/**
* #param mixed $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* #return mixed
*/
public function getAuthor()
{
return $this->author;
}
/**
* #param mixed $author
*/
public function setAuthor(Author $author)
{
$this->author = $author;
}
}
Guilhem is right, the default Symfony ObjectNormalizer isn't able to normalize properties of non scalar types for now.
However, this feature is being added in Symfony 3.1: https://github.com/symfony/symfony/pull/17660
In the meantime, you can copy/paste the ObjectNormalizer version provided in the PR linked above in your project.
You can also take a look at a similar implementation available in API Platform:
https://github.com/api-platform/core/blob/master/src/Bridge/Symfony/PropertyInfo/Metadata/Property/PropertyInfoPropertyMetadataFactory.php#L48-L51
https://github.com/api-platform/core/blob/master/src/JsonLd/Serializer/ItemNormalizer.php#L155-L178
The symfony serializer can't denormalize complex properties.
I think that the only way to do that is to manage your object denormalization by yourself:
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class Author implements DenormalizableInterface {
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = array()) {
if (isset($data['name'])) {
$this->setName($data['name']);
}
if (isset($data['books']) && is_array($data['books'])) {
$books = array();
foreach ($data['books'] as $book) {
$books[] = $denormalizer->denormalize($book, Book::class, $format, $context);
}
$this->setBooks($books);
}
}
// ...
}
You can also create a custom normalizer but this is more complicated (you can take a look at this article which explains more or less how to do that).
I hope this will help you :-)
I'm almost getting desperate. I have made this question few times hoping for new answers to come, but all of them did not help me. I have tried many times but I only get errors, based on these answers, but no success. I'm stuck at it for more than a week, and I have a limited time.
I have a table called Client, related to Budget. First, showing the Client entity:
class Client
{
private $id;
private $name;
private $phone;
private $email;
private $streetName;
private $district;
private $number;
private $city;
private $zipCode;
private $budget;
public function __toString()
{
return $this->name;
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setPhone($phone)
{
$this->phone = $phone;
return $this;
}
public function getPhone()
{
return $this->phone;
}
public function setEmail($email)
{
$this->email = $email;
return $this;
}
public function getEmail()
{
return $this->email;
}
public function setStreetName($streetName)
{
$this->streetName = $streetName;
return $this;
}
public function getStreetName()
{
return $this->streetName;
}
public function setDistrict($district)
{
$this->district = $district;
return $this;
}
public function getDistrict()
{
return $this->district;
}
public function setNumber($number)
{
$this->number = $number;
return $this;
}
public function getNumber()
{
return $this->number;
}
public function setCity($city)
{
$this->city = $city;
return $this;
}
public function getCity()
{
return $this->city;
}
public function setZipCode($zipCode)
{
$this->zipCode = $zipCode;
return $this;
}
public function getZipCode()
{
return $this->zipCode;
}
function setBudget($budget)
{
$this->budget = $budget;
}
function getBudget()
{
return $this->budget;
}
}
And now, the Budget entity:
class Budget
{
private $id;
private $clientId;
private $address;
private $installments;
private $checkDays;
private $dateStart;
private $dateCreated;
private $totalValue;
public function __construct()
{
$this->dateCreated = new \DateTime();
}
public function getId()
{
return $this->id;
}
public function setClientId(Client $clientId)
{
$this->clientId = $clientId;
return $this;
}
public function getClientId()
{
return $this->clientId;
}
public function setAddress($address)
{
$this->address = $address;
return $this;
}
public function getAddress()
{
return $this->address;
}
public function setInstallments($installments)
{
$this->installments = $installments;
return $this;
}
public function getInstallments()
{
return $this->installments;
}
public function setCheckDays($checkDays)
{
$this->checkDays = $checkDays;
return $this;
}
public function getCheckDays()
{
return $this->checkDays;
}
public function setDateStart($dateStart)
{
$this->dateStart = $dateStart;
return $this;
}
public function getDateStart()
{
return $this->dateStart;
}
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
return $this;
}
public function getDateCreated()
{
return $this->dateCreated;
}
public function setTotalValue($totalValue)
{
$this->totalValue = $totalValue;
return $this;
}
public function getTotalValue()
{
return $this->totalValue;
}
}
Fine. Now, their relationship. First, the Client.orm.yml:
CDG\PanelBundle\Entity\Client:
type: entity
table: client
repositoryClass: CDG\PanelBundle\Entity\ClientRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
name:
type: string
length: 255
phone:
type: string
length: 255
email:
type: string
length: 255
streetName:
type: string
length: 255
column: street_name
district:
type: string
length: 255
number:
type: string
length: 255
city:
type: string
length: 255
zipCode:
type: string
length: 255
column: zip_code
oneToMany:
budget:
targetEntity: Budget
mappedBy: clientId
lifecycleCallbacks: { }
And now, the Budget.orm.yml:
CDG\PanelBundle\Entity\Budget:
type: entity
table: budget
repositoryClass: CDG\PanelBundle\Entity\BudgetRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
address:
type: string
length: 255
installments:
type: integer
checkDays:
type: integer
column: check_days
dateStart:
type: datetime
column: date_start
dateCreated:
type: datetime
column: date_created
totalValue:
type: decimal
column: total_value
nullable: true
manyToOne:
clientId:
targetEntity: Client
inversedBy: budget
joinColumn:
name: client_id
referencedColumnName: id
lifecycleCallbacks: { }
So far so good. Explaining, I must update the field budget from Client entity right after inserting new data into Budget. For this, I have tried MANY different ways, erros, but sadly I don't remember all of them. If you feel like, please check my previous questions here, here and if you are Brazilian, here, to see the how I have tried to solve this.
Below is my current BudgetController.php and its addAction() method, used to persist the form data into Budget table. The way it is now gives no errors, but the budget field from Client is not updated.
public function addAction(Request $request)
{
$form = $this->createForm(new BudgetType());
$manager = $this->getDoctrine()->getManager();
$Client = $manager->getRepository('PanelBundle:Client');
$Budget = $manager->getRepository('PanelBundle:Budget');
if ($request->getMethod() == 'POST') {
$form->handleRequest($request);
if ($form->isValid()) {
$manager->persist($form->getData());
$manager->flush();
$Client = $manager->find('PanelBundle:Client', $form['client_id']->getData()->getId());
$Client->setBudget($manager->getRepository('PanelBundle:Budget')->getLastId());
$this->addFlash('success', 'Novo orçamento adicionado');
return $this->redirect($this->generateUrl('panel_budgets'));
}
}
return $this->render('PanelBundle:Budget:add.html.twig', array(
'clients' => $Client->findAll(),
'form' => $form->createView()
));
}
If I try to persist the $Client and flush(), I get the error:
Warning: spl_object_hash() expects parameter 1 to be object, string given
I have written a function addBudgetToClient() in my BudgetRepository.php, that has two parameters, one for the selected Client, and other for the recently added Budget ids. Here it is:
public function addBudgetToClient($clientId, $budgetId)
{
$qb = $this->createQueryBuilder('PanelBundle:Client')
->update('PanelBundle:Client', 'c')
->set('c.budget', $budgetId)
->where('c.id = ' . $clientId)
->getQuery()
->execute();
}
Then, in my Controller, if I try:
$Budget->addBudgetToClient($form['client_id']->getData()->getId(), $Budget->getLastId());
I get the error:
Semantical Error] line 0, col 34 near 'budget = 4 WHERE': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected.
[2/2] QueryException: [Semantical Error] line 0, col 34 near 'budget = 4 WHERE': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected. +
[1/2] QueryException: UPDATE PanelBundle:Client c SET c.budget = 4 WHERE c.id = 3 +
The SQL seems right, then why?
It is many errors, many ways, but zero success. Thank you.
EDITED
As requested, my getLastId():
public function getLastId()
{
return $this->createQueryBuilder('b')
->select('MAX(b.id)')
->getQuery()
->getSingleScalarResult();
}
You are storing your entity in a wrong way. Instead, try this:
$budget = new Budget();
$form = $this->createForm(new BudgetType(), $budget, array("method" => "POST");
$manager = $this->getDoctrine()->getManager();
$form->handleRequest($request);
if ($form->isValid()) {
$form->handleRequest($request);
$manager->persist($budget);
$client = $manager->find('PanelBundle:Client', $form['client_id']->getData()->getId());
// do something if was not found
$client->setBudget($budget);
$manager->flush();
$this->addFlash('success', 'Novo orçamento adicionado');
return $this->redirect($this->generateUrl('panel_budgets'));
}
i have two entities Topic
and TopicContent
when i send data from Topic entity, all ok. But when i included TopicContentType to my form builder, a have an error.. Friends, help me please..
My controller
public function createAction(Request $request)
{
$entity = new Topic();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('application_club_show', array('id' => $entity->getTopicId())));
}
return $this->render('ApplicationClubBundle:Topic:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
$builder
->add('user_id', 'hidden')
->add('topic_title')
->add('topic_tags')
->add('topic_publish', 'hidden')
->add('topic_user_ip', 'hidden')
->add('topic_count_read', 'hidden')
->add('topic_count_comment', 'hidden')
;
$builder->add('topic_content', new \Application\ClubBundle\Form\TopicContentType());
TypicType
$builder
->add('user_id', 'hidden')
->add('topic_title')
->add('topic_tags')
->add('topic_publish', 'hidden')
->add('topic_user_ip', 'hidden')
->add('topic_count_read', 'hidden')
->add('topic_count_comment', 'hidden')
;
$builder->add('topic_content', new \Application\ClubBundle\Form\TopicContentType());
TopicContentType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('topic_id')
->add('topic_text')
;
}
TopicContent Entity
<?php
namespace Application\ClubBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
class TopicContent
{
/**
*
* #var integer
*/
private $topic_id;
/**
* #var string
*/
private $topic_text;
/**
* #var \Application\ClubBundle\Entity\Topic
*/
private $topic;
/**
* Set topic_id
*
* #param integer $topicId
* #return TopicContent
*/
public function setTopicId($topicId)
{
$this->topic_id = $topicId;
return $this;
}
/**
* Get topic_id
*
* #return integer
*/
public function getTopicId()
{
return $this->topic_id;
}
/**
* Set topic_text
*
* #param string $topicText
* #return TopicContent
*/
public function setTopicText($topicText)
{
$this->topic_text = $topicText;
return $this;
}
/**
* Get topic_text
*
* #return string
*/
public function getTopicText()
{
return $this->topic_text;
}
/**
* Set topic
*
* #param \Application\ClubBundle\Entity\Topic $topic
* #return TopicContent
*/
public function setTopic(\Application\ClubBundle\Entity\Topic $topic = null)
{
$this->topic = $topic;
return $this;
}
/**
* Get topic
*
* #return \Application\ClubBundle\Entity\Topic
*/
public function getTopic()
{
return $this->topic;
}
}
Topic Entity
/**
* Add topic_content
*
* #param \Application\ClubBundle\Entity\TopicContent $topicContent
* #return Topic
*/
public function setTopicContent(\Application\ClubBundle\Entity\TopicContent $topicContent)
{
$this->topic_content[] = $topicContent;
return $this;
}
my entities relatoins
Topic
Application\ClubBundle\Entity\Topic:
type: entity
table: topic
id:
topic_id:
type: integer
generator: { strategy: AUTO }
fields:
topic_title:
type: string
length: 200
oneToMany:
topic_content:
targetEntity: TopicContent
mappedBy: topic
topic_tag:
targetEntity: TopicTag
mappedBy: topic
cascade: ["persist"]
TopicContent
Application\ClubBundle\Entity\TopicContent:
type: entity
table: topic_content
id:
topic_id:
type: integer
fields:
topic_text:
type: text
oneToOne:
topic:
targetEntity: Topic
inversedBy: topic_content
joinColumn:
name: topic_id
referencedColumnName: topic_id
cascade: ["persist"]
You have the answer in the error message displayed by Symfony: you need to persist the Topic entity AND the TopicContent entity. To do that you must define a cascade={"persist"} on the relation.
Edit:
Another way to persist the TopicContent would be to do it explicitly in the form processing function:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$content = $entity->getTopicContent();
$em->persist($entity);
$em->persist($content);
$em->flush();
return $this->redirect($this->generateUrl('application_club_show', array('id' => $entity->getTopicId())));
}
I'm using Symfony2.2 with StofDoctrineExtensionsBundle (and so Gedmo DoctrineExtensions).
I've a simple entity
/**
* #ORM\Entity
* #Gedmo\Loggable
* #ORM\Table(name="person")
*/
class Person {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
[...]
/**
* #ORM\Column(type="datetime", nullable=true)
* #Assert\NotBlank()
* #Assert\Date()
* #Gedmo\Versioned
*/
protected $birthdate;
}
When changing an attribute for an existing object, a log entry is done in table ext_log_entries. An entry in this log table contains only changed columns. I can read the log by:
$em = $this->getManager();
$repo = $em->getRepository('Gedmo\Loggable\Entity\LogEntry');
$person_repo = $em->getRepository('Acme\MainBundle\Entity\Person');
$person = $person_repo->find(1);
$log = $repo->findBy(array('objectId' => $person->getId()));
foreach ($log as $log_entry) { var_dump($log_entry->getData()); }
But what I don't understand is, why the field birthdate is always contained in a log entry, even it's not changed. Here some examples of three log entries:
array(9) {
["salutation"]=>
string(4) "Herr"
["firstname"]=>
string(3) "Max"
["lastname"]=>
string(6) "Muster"
["street"]=>
string(14) "Musterstraße 1"
["zipcode"]=>
string(5) "00000"
["city"]=>
string(12) "Musterhausen"
["birthdate"]=>
object(DateTime)#655 (3) {
["date"]=>
string(19) "1893-01-01 00:00:00"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Europe/Berlin"
}
["email"]=>
string(17) "email#example.com"
["phone"]=>
NULL
}
array(2) {
["birthdate"]=>
object(DateTime)#659 (3) {
["date"]=>
string(19) "1893-01-01 00:00:00"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Europe/Berlin"
}
["phone"]=>
string(9) "123456789"
}
array(2) {
["birthdate"]=>
object(DateTime)#662 (3) {
["date"]=>
string(19) "1893-01-01 00:00:00"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Europe/Berlin"
}
["phone"]=>
NULL
}
I want to log only really changed data. Is there any option I've not seen yet? It seems to be related to the fact, that birthdate is a DateTime object, doesn't it?
EDIT
It is not related to the DateTime object. This occurs even in other entities. I've another entity containing a simple value:
/**
* #ORM\Entity
* #Gedmo\Loggable
* #ORM\Entity(repositoryClass="Acme\MainBundle\Repository\ApplicationRepository")
* #ORM\Table(name="application")
*/
class Application {
[...]
/**
* #ORM\Column(type="integer")
* #Assert\NotBlank(groups={"FormStepOne", "UserEditApplication"})
* #Gedmo\Versioned
*/
protected $insurance_number;
}
When opening the edit form in browser an saving without modification, the log table contains:
update 2013-04-26 11:32:42 Acme\MainBundle\Entity\Application a:1:{s:16:"insurance_number";s:7:"1234567";}
update 2013-04-26 11:33:17 Acme\MainBundle\Entity\Application a:1:{s:16:"insurance_number";s:7:"1234567";}
Why?
This might be a similar issue to the one I encountered when using another of these extensions (timestampable), namely: that the default change tracking policy used in doctrine (it tries to auto detect changes) sometimes marks entities as dirty, when they are not (for me this was happening when my entity contained a datetime object, which is understandable given that this is an object which needs to be constructed when pulling it from the database). This isn't a bug or anything - it's expected behaviour and there are a few ways around it.
Might be worth trying to implement an alternative change tracking policy on the entities you want to log and seeing if that fixes things - I would guess that this behaviour (logging) doesn't kick in unless the entity state is dirty, which you can avoid by implementing change tracking yourself manually:
http://docs.doctrine-project.org/en/latest/cookbook/implementing-the-notify-changetracking-policy.html
Don't forget to update your entity:
YourBundle\Entity\YourThing:
type: entity
table: some_table
changeTrackingPolicy: NOTIFY
See this thread:
https://github.com/Atlantic18/DoctrineExtensions/issues/333#issuecomment-16738878
I also encountered this problem today and solved it. Here is complete solution, working for all string, float, int and DateTime values.
Make your own LoggableListener and use it instead of Gedmo Listener.
<?php
namespace MyBundle\Loggable\Listener;
use Gedmo\Loggable\LoggableListener;
use Gedmo\Tool\Wrapper\AbstractWrapper;
class MyLoggableListener extends LoggableListener
{
protected function getObjectChangeSetData($ea, $object, $logEntry)
{
$om = $ea->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $om);
$meta = $wrapped->getMetadata();
$config = $this->getConfiguration($om, $meta->name);
$uow = $om->getUnitOfWork();
$values = [];
foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
if (empty($config['versioned']) || !in_array($field, $config['versioned'])) {
continue;
}
$oldValue = $changes[0];
if ($meta->isSingleValuedAssociation($field) && $oldValue) {
if ($wrapped->isEmbeddedAssociation($field)) {
$value = $this->getObjectChangeSetData($ea, $oldValue, $logEntry);
} else {
$oid = spl_object_hash($oldValue);
$wrappedAssoc = AbstractWrapper::wrap($oldValue, $om);
$oldValue = $wrappedAssoc->getIdentifier(false);
if (!is_array($oldValue) && !$oldValue) {
$this->pendingRelatedObjects[$oid][] = [
'log' => $logEntry,
'field' => $field,
];
}
}
}
$value = $changes[1];
if ($meta->isSingleValuedAssociation($field) && $value) {
if ($wrapped->isEmbeddedAssociation($field)) {
$value = $this->getObjectChangeSetData($ea, $value, $logEntry);
} else {
$oid = spl_object_hash($value);
$wrappedAssoc = AbstractWrapper::wrap($value, $om);
$value = $wrappedAssoc->getIdentifier(false);
if (!is_array($value) && !$value) {
$this->pendingRelatedObjects[$oid][] = [
'log' => $logEntry,
'field' => $field,
];
}
}
}
//fix for DateTime, integer and float entries
if ($value == $oldValue) {
continue;
}
$values[$field] = $value;
}
return $values;
}
}
For Symfony application, register your listener in config.yml file.
stof_doctrine_extensions:
orm:
default:
loggable: true
class:
loggable: MyBundle\Loggable\Listener\MyLoggableListener
If you are using DateTime fields in your entities, but in database you store only date, then you also need to reset time part in all setters.
public function setDateValue(DateTime $dateValue = null)
{
$dateValue->setTime(0, 0, 0);
$this->dateValue = $dateValue;
return $this;
}
That should do the job.
For \DateTime I am still working on it but for the second part of your question there is a way that solved my problem with my Numeric properties:
/**
* #ORM\Entity
* #Gedmo\Loggable
* #ORM\Entity(repositoryClass="Acme\MainBundle\Repository\ApplicationRepository")
* #ORM\Table(name="application")
*/
class Application {
[...]
/**
* #ORM\Column(type="integer")
* #Assert\NotBlank(groups={"FormStepOne", "UserEditApplication"})
* #Gedmo\Versioned
*/
protected $insurance_number;
}
Here you declare insurance_number as an integer property but as we know PHP has no type and does dynamic casting which is a conflicting thing with Gedmo Loggable.
To solve, just make sure that you are doing explicit casting yourself either in your Setter Method or in your Business Logic.
for instance replace this(Business Logic):
$application->setInsuranceNumber($valueComeFromHtmlForm)
with this one:
$application->setInsuranceNumber( (int)$valueComeFromHtmlForm)
Then when you persist your object you will not see any records in your logs.
I think this is because Loggable or Doctrine Change Tracker expects Integer and receives String (which is a 'not casted Integer') and So it marks the property dirty. We can see it in Log Record (S denotes that the new value is String.)