Intro
So, i have an entity Presence that is watched by Doctrine.
I also have another entity Decision that is not watched by Doctrine.
The Presence entity has an attribute $decisions, typed ArrayCollection and contains 0 to n Decision entities.
Since i don't store each Decision object that is contained within the ArrayCollection, i typed the attribute $decision as Doctrine Array and everything is working so far.
I can get the whole Presence object back from database & it contains every Decision objects that i added to it.
The problem
When i edit one Decision object that is present in the ArrayCollection of my Presence object like :
foreach($Presence->getDecisions() as $decision) {
$decision->setMorning($value);
}
The modifications are applied locally, and i can see them when doind a dump($Presence);
Presence {#2354 ▼
#id: 8
#decisions: ArrayCollection {#1409 ▼
-elements: array:6 [▼
0 => Decision {#1408 ▼
#token_id: "005867b2915438c03383bb831d87c26346c586d6ecd46b735c7a23b4fabb682a8ac34a90ea3d53cc55c2209deaa83337abeb2b98ded43967fcfb0f4d04d5ada6"
#date: DateTime #1531692000 {#1407 ▶}
#morning: -1
#afternoon: 0
}
1 => Decision {#1406 ▶}
2 => Decision {#1404 ▶}
3 => Decision {#1402 ▶}
4 => Decision {#1400 ▶}
5 => Decision {#1398 ▶}
]
}
#last_edit_date: DateTime #1532109183 {#2454 ▼
date: 2018-07-20 19:53:03.062940 Europe/Berlin (+02:00)
}
#user: User {#1393 ▶}
}
On this dump() you can see that the attribute #morning: -1 is the result of the setter call.
But when i try to flush the changes with $this->entityManager->flush(); the ArrayCollection has not been modified.
At first i thought it was a problem of changes detection, so i added a #last_edit_date DateTime field onto my Presence entity.
The fun part is that this attribute is updating just fine when flushing.
It seems that the ArrayCollection won't update for some reason.
UPDATE
To complete my post here is additional informations :
The full method in the controller
/**
* #Route(name="edit_decision", path="/edit-decision/{tokenDate}/{dayPart}/{newValue}")
*/
public function EditDecisionAction(Request $request)
{
$token = $request->get('tokenDate');
$dayPart = $request->get('dayPart');
$newValue = intval($request->get('newValue'));
$myPresence = $this->em->getRepository('AppBundle:Presence')->findOneBy([
'user' => $this->connectedUser
]);
/** #var ArrayCollection $decisionCollection */
$decisionCollection = $myPresence->getDecisions();
$found = FALSE;
/** #var Decision $decision */
foreach ($decisionCollection as $decision) {
if ($decision->getTokenId() == $token) {
$found = TRUE;
if ($dayPart === 'morning') {
$decision->setMorning($newValue);
}elseif ($dayPart === 'afternoon') {
$decision->setAfternoon($newValue);
}
break;
}
}
if (!$found) {
throw $this->createNotFoundException();
}
// I set a new Date to trigger a modification on the Presence Entity : It works
$myPresence->setLastEditDate(new \DateTime());
// Here is the dump that you can see above
dump($myPresence);
$this->em->flush();
try{
$this->em->flush();
return $this->json([
'status' => TRUE
]);
}catch (\Exception $exception) {
return $this->json([
'status' => FALSE
]);
}
}
The attribute that contains the ArrayCollection :
/**
* Doctrine Type Array
*
* #ORM\Column(type="array", nullable=true)
*/
protected $decisions;
Any help or even hints are welcome !
Thanks :)
UPDATE 2
I may have found the solution thanks to this post : How to force Doctrine to update array type fields?
The problem comes from the doctrine Array type under which i store my objects :
Doctrine uses identical operator (===) to compare changes between old and new values. The operator used on the same object (or array of objects) with different data always return true. There is the other way to solve this issue, you can clone an object that needs to be changed.
Update 3
Nope still doesn't persists the changes made on the ArrayCollection's items in the database.
Gonna try Cloning : Adding : Removing the old one and we will se what happens.
FINAL UPDATE
Ok so i finally got it to work using a trick.
The main problem was the behaviour of Doctrine changes detection towards the Array type.
As quoted above, doctrine does : Array === Array to check if there is changes to be made.
Kinda weird because when you ADD or REMOVE an element from your ArrayCollection, for Doctrine the collection is still equivalent to the precedent state.
There should be a way to test equivalences between 2 objects like in Java .equals to remove those ambiguities.
Anyway what i did to solve it :
I store my collection to an initial state :
$decisionCollection = $myPresence->getDecisions();
I place my setters & modify my collection accordingly :
foreach ($myPresence->getDecisions() as $key => $decision) {
if ($decision->getTokenId() == $token) {
if ($dayPart === 'morning') {
$decision->setMorning($newValue);
} elseif ($dayPart === 'afternoon') {
$decision->setAfternoon($newValue);
}
break;
}
}
And then the Trick :
$myPresence->unsetDecisions();
$this->em->flush();
I unset completly my collection ($this->collection = NULL) & flush a first time.
It triggers the Update since Array !== NULL
And then i set my collection with the updated one that contains the setted objects :
$myPresence->setDecisions($decisionCollection);
$myPresence->setLastEditDate(new \DateTime());
And flush a last time to keep the new changes.
Thanks everyone for your time & your comments !
Related
i want to make website which has 'like' and 'dislike'
i made controller
public function addLike($id)
{
$feeling = new Feeling();
$feeling->post_id = $id;
$feeling->user_id = Auth::user()->id;
$feeling->like = 1;
$feeling->check = 1;
if ($dd = DB::table('feelings')->where('post_id', $feeling->post_id)->where('user_id', Auth::user()->id)->get()) {
dd($dd);
return redirect()->route('post.show', ['feeling' => $feeling, 'id' => $id]);
} else {
$feeling->save();
return redirect()->route('post.show', ['feeling' => $feeling, 'id' => $id]);
}
}
i thought if(feeling is $feeling = new Feeling, and Feeling is my like,dislike model)
when feeling's post number and user number exist on feelings table,
i just return redirect
else, if post number and user number both doesn't exist together
i want to save and direct
but i have problem ->>>
when post number and user number both doesn't exist together,
i checked my web doesn't work properly so i put dd($dd), and saw
Illuminate\Support\Collection {#360 ▼
#items: []
}
$dd = DB::table('feelings')->where('post_id', $feeling->post_id)->where('user_id', Auth::user()->id)
dd($dd)
had made this kind of empty array.
how can i check feeling's post number and user number's existence?
An empty collection would still be evaluated as true inside the if statement. There are different ways to check for the existence of records in Larave.
One is exists()
$exists = DB::table('feelings')->where('post_id', $feeling->post_id)->where('user_id', Auth::user()->id)->exists();
//$exists returns true is record exists, otherwise false
Check if record collections is empty or not using isEmpty() or isNotEmpty():
$empty = DB::table('feelings')->where('post_id', $feeling->post_id)->where('user_id', Auth::user()->id)->get()->isEmpty();
//$empty returns true if no result
Refer here to check available methods:
https://laravel.com/docs/8.x/collections
In my HTML frontend, I have a jQuery DataTable displaying all records fetched via AJAX from the database - a rather pretty straight forward thing. I use the Laravel Collection's ->transform(function($o){ . . . }) to iterate through the collection and return it in an array-esque manner. Just think of the following piece of code in a controller:
$cAllRecords = DatabaseRecord::all();
if(!empty($aData['sFilterIds']))
{
$cAllRecords = $cAllRecords->whereIn('creator', explode(',', $aData['sFilterIds']));
}
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})]);
I'm actually just filtering for records created by certain user IDs (the whereIn() call in line 4. However, the response sent back to the client looks different for different users filtered leading the jQuery table to show 'no records available', as it had received an malformed answer from the server. For one user, the response looks like this:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
]
]
}
This is a correctly formed server response and will show up in the table regularly. Great! Now something that drives me insane - the same code for another user (ID 1, while the first request was for user ID 2) returns this:
{
"data":{
"1":[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
}
}
which, pretty obviously, is malformed and is not correctly parsed by the datatable. OK, now combing them two filters and filtering for user ID 1 and 2 will, again, return the response correctly formatted:
{
"data":[
[
1,
"2019-05-29 16:44:53",
"2019-05-29 16:44:53",
"<a href=\"#\">edit<\/a>"
],
[
3,
"2019-05-29 17:08:49",
"2019-05-29 17:08:49",
"<a href=\"#\">edit<\/a>"
]
]
}
I tried a number of things, none of which had worked since it's merely guessing why it could work with one user and not with another. (Things like reversing the order of IDs to be filtered, etc., but I found out that the filtering is not the problem. It MUST be the transform, which behaves inconsistent.)
Any ideas on why this happens and how to tackle it? I mean, it's not the only way to achieve what I'm after, I was using ->each() and array_push for all the time before but wanted to get rid of it for the sake of making use of Laravel's helpers (or possibilites) - the manual iteration and array pushing process worked out seamlessly before, and even other parts of the app work well with the Collection transform over array iteration and pushing. Why doesn't it here?
Update: The ->map() collection method behaves exactly same. Map, as opposed by transform, does not alter the collection itself. However, this should not be a relevant part within this application any way. I really can't understand what's going wrong. Is this possibly Laravel's fault?
Please note that transform method returns a Illuminate\Support\Collection.
It's better that you call all() after the transform to get an array result.
Like this:
...
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->all()]);
#Cvetan Mihaylov's answer made me look at all the available collection methods (https://laravel.com/docs/5.8/collections#available-methods) and I found ->values() to return the values reindexed. And - that did the trick! :-)
return response()->json(['data' => $cAllRecords->transform(function ($oDatabaseRecord) {
/** #var $oDatabaseRecord DatabaseRecord */
$sActionsHtml = 'edit';
$sUrl = route('some.route', ['iDatabaseRecordId' => $oDatabaseRecord->getAttribute('od')]);
return [
$oDatabaseRecord->getAttribute('id'),
$oDatabaseRecord->getAttribute('updated_at')->toDateTimeString(),
$oDatabaseRecord->getAttribute('created_at')->toDateTimeString(),
$sActionsHtml
];
})->values()]);
I'm using route binding to determine if each part of the URL is related to the previous. I'm trying to access the route parameter/variable ($stage_number) in my query builder but no luck. To confuse things, if I substitute the variable with a hard value it works, e.g. 4
How do I use the $stage_number variable in my query?
/*
* Route
*/
Route::get('/application/{application_id}/stage/{stage_number}', [
'as' => 'application.stage',
'uses' => 'ApplicationController#stage'
]);
/*
* Route Service Provider
*/
// Application
$router->bind('application_id', function($application_id)
{
$application = Application::find($application_id);
if (! $application)
{
abort(404);
}
return $application;
});
// Stage
$router->bind('stage_number', function($stage_number)
{
$application = $this->getCurrentRoute()->application_id;
$stage = collect($application->stages)->where('order', $stage_number)->all();
if (! $stage)
{
abort(404);
}
return $stage;
});
Update in response to patricus:
Thanks for the information about calling where() on collections; I did not realise it handled differently to the query builder. Updating my where() for the collection works perfectly - thanks. However, I am still having trouble when using the query builder:
// Route with data
/application/1/stage/4
// Actual data returned
array:4 [▼
0 => array:2 [▼
"name" => "Information about the University or Education Course"
"order" => 3
]
1 => array:2 [▼
"name" => "Information about your education to date"
"order" => 4
]
2 => array:2 [▼
"name" => "Other Information"
"order" => 5
]
3 => array:2 [▼
"name" => "Declaration"
"order" => 6
]
]
// Desired data to be returned
array:1 [▼
0 => array:2 [▼
"name" => "Information about your education to date"
"order" => 4
]
]
Regardless of what order I specify in my route I seem to get everything returned that is not null unless I choose 1 or 2 (which are rows with no order number and excluded from the array example above) and then I get that row returned with all of the other rows that is not null (as shown in the example above) but any other null rows are excluded. Is this a problem caused by the relationship on my Application object?
public function stages()
{
return $this->hasMany('App\Entities\Application\Stage', 'type_id')->orWhereNull('type_id')->orderBy('order', 'asc');
}
Update:
Setting the foreign_key and local_key on my relationship seemed to resolve the other issues:
public function stages()
{
return $this->hasMany('App\Entities\Application\Stage', 'type_id', 'type_id')->orWhereNull('type_id')->orderBy('order', 'asc');
}
You're actually calling the where() method on a Collection object, not on a Builder object.
The where() method on the Collection works a little differently. First, it can only do an equals comparison. Second, by default, it does a strict equals (===) comparison, and this is your issue. Since your $stage_number is a string, and your order field is most likely an integer, the strict comparison doesn't return any results.
You can change the comparison to a loose equals (==) by passing false as the third parameter. You can also use the whereLoose() method, which does the same thing.
Also note, assuming that stages is a hasMany or belongsToMany relationship on the Application object, there is no need for the call to collect(). $application->stages will already return a Collection.
// pass false as third parameter
$stage = $application->stages->where('order', $stage_number, false)->all();
// or use whereLoose
$stage = $application->stages->whereLoose('order', $stage_number)->all();
Now, having said all that, you probably want to be calling where() on the query builder. While $application->stages will give a Collection of related stage objects, $application->stages() returns the Relation object for the relationship, which gives you access to the query builder. Since you have access to the builder, you can add your where clause to the query being run and will provide a performance boost (and also doesn't care about variable type).
$stage = $application->stages()->where('order', $stage_number)->get();
Note that get() will return a Collection object that contains the matching stage objects. If order is unique and you want to get that one stage object, you can use first() instead of get():
$stage = $application->stages()->where('order', $stage_number)->first();
I'm using a twig for loop to display a list of elements. These elements come from a decoded json array, from an API.
I have a OneToMany relation between my user and these elements.
User needs to chose one of these elements, which will be added to the user with the addElement() function.
I tried to do so using a Symfony2 form in the loop, but it is only displayed on the first element. I also tried using a link to a controller function, but since none of these elements are persisted in my DB, I got this error:
"Unable to guess how to get a Doctrine instance from the request information."
Here's how I display my elements:
{% block itinerary %}
{% for element in elements %}
<aside class="flights-results__by-price col-md-3">
<span class="flights-results__price">{{ element.price ? element.price : 'Unknown' }}</span>
Delete
</aside>
{% endfor %}
{% endblock itinerary %}
Here is the function where I create and fill my elements :
public function getAvailabilities($availabilities, $planes, $airports)
{
$reservations = array();
foreach ($availabilities as $ar)
{
$leg = new Leg();
$leg->getId();
foreach($ar as $a)
{
$leg = $this->fillLeg($leg, $a);
foreach($a->availabilities as $aleg)
{
$leg->setAirplaneType($this->findPlane($planes, $aleg->airplane_type_id));
$leg->setAirportStart($this->findAirport($airports, $a->lfi_from));
$leg->setAirportEnd($this->findAirport($airports, $a->lfi_to));
$leg->setDurationLeg($aleg->duration);
$leg->setEndHour($aleg->datetime_to);
}
$startdate = $a->datetime;
}
$reservations[] = $leg;
}
return $reservations;
}
and here is the result when I dump($elements) :
FlightController.php on line 55:
array:4 [▼
0 => {#953 ▼
+"3e1f975601f59090decc8f2d5ced72010162e48e": {#954 ▼
+"lfi_from": "FR58957"
+"lfi_to": "FR45300"
+"datetime": "2015-09-10 20:00:00"
+"nb_pax": "4"
+"availabilities": array:1 [▼
0 => {#955 ▶}
]
}
}
1 => {#956 ▼
+"3e1f975601f59090decc8f2d5ced72010162e48e": {#957 ▼
+"lfi_from": "FR45300"
+"lfi_to": "AG00060"
+"datetime": "2015-09-10 23:00:00"
+"nb_pax": "4"
+"availabilities": array:1 [▼
0 => {#958 ▶}
]
}
}
2 => {#959 ▼
+"3e1f975601f59090decc8f2d5ced72010162e48e": {#960 ▼
+"lfi_from": "FR45300"
+"lfi_to": "AG00060"
+"datetime": "2015-11-30 23:00:00"
+"nb_pax": "4"
+"availabilities": array:1 [▼
0 => {#961 ▶}
]
}
}
3 => {#962 ▼
+"3e1f975601f59090decc8f2d5ced72010162e48e": {#963 ▼
+"lfi_from": "FR45300"
+"lfi_to": "OLOLOL"
+"datetime": "2015-09-18 23:00:00"
+"nb_pax": "2"
+"availabilities": array:1 [▼
0 => {#964 ▶}
]
}
}
]
The main problem is that the API returns several thousands results. For obvious reasons, I cannot persist them all.
I guess the easiest way to ask would be "What is the best way to send datas on an entity to another function in my controller, without persisting this entity?". So far, I've always worked with persisted elements, with an id as identifier, but I realize it gets trickier when we deal with non-persisted entities.
If you have a OneToMany relation between your User and these Elements, it means the Elements are persisted somehow. So why can't you use the id of the element ?
In case you persist it, you may need to add a ParamConverter in your controller code somewhere along those lines:
/**
* #Route("/selectLeg/{element}")
* #ParamConverter("element", class="YourBundle:Element", options={"mapping": {"name": "element.whatever_param"}})
* #Template()
*/
public function selectLegAction(Element $element)
The fact is, if Symfony2 doesn't know about your Element entity, you won't be able to do addElement() to your User.
What I guess is that you get the elements' list in the frontend and then try to update your User object. In this case I would json_encode your element in Twig (it's a simple array after all if I understand) :
Select this leg
and create a new Element in your controller :
/**
* #Route("/selectLeg/{legAsJSONString}")
*/
public function selectLegAction($legAsJSONString) {
$e = json_decode($legAsJSONString);
$leg = new Leg();
$leg->setWhateverParameter($e->parameter_in_the_array);
// more parameters here
$em->persist($leg)->flush();
/// Now here you have $leg->getId(); if ever you need it
}
EDIT : Adapted to your comment. If you don't need to persist the element (leg) before the user has chosen a particular one, then send the element in string form in a GET parameter, in the route parameters, or in the data of a POST request (cleaner solution). You don't need the id since you can pass the full object in the request, as a JSON string.
Remove your useless $leg->getId(); from getAvailabilities() as well, it doesn't actually do anything, and the id doesn't exist anyway.
If I miss the point and that the $leg object is too complicated and therefore not serializable in JSON, then you will need to persist it since two consequent requests will need to have access to it.
In my symfony2/doctrine2 application, I have a weird case where when persisting a modified entity, the change is not flushed to the database, and I can't understand why.
Below is my code :
$date = $subscription->getPaymentValidUntil()->format('d/m/Y');
$period = $payment->getDetail('period');
$validDate = $subscription->getPaymentValidUntil()
->add(new\DateInterval($period == Subscription::MONTH ? 'P1M' : 'P1Y'))
;
$subscription->setPaymentValidUntil($validDate);
$this->em->persist($subscription);
exit(var_dump(array(
$date,
$this->em->getUnitOfWork()->getScheduledEntityUpdates(),
$subscription->getPaymentValidUntil(),
)));
$this->em->flush();
The output of the vardump is the following:
array (size=3)
0 => string '12/05/2015' (length=10)
1 =>
array (size=0)
empty
2 =>
object(DateTime)[2295]
public 'date' => string '2015-06-12 18:52:37' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Paris' (length=12)
If I flush before the vardump or remove the vardump, indeed, the date value does not change in my database. Why?
As you can see, I add one month to this date value, it's reflected in the entity, but it's not scheduled for update. How can I solve this ?
EDIT : I've digged into the entity manager persist function and I figured out that when dumping the entity in Doctrine\ORM\EntityManager::persist the date was good but when dumping it in Doctrine\ORM\UnitOfWork::persist, the date had somehow been reseted to its original one :
Doctrine\ORM\EntityManager::persist : dumps the updated entity
public function persist($entity)
{
if ( ! is_object($entity)) {
throw ORMInvalidArgumentException::invalidObject('EntityManager#persist()' , $entity);
}
$this->errorIfClosed();
if ($entity instanceof Subscription) exit(var_dump($entity));
$this->unitOfWork->persist($entity);
}
Doctrine\ORM\UnitOfWork::persist : dumps the non-modified entity : WHY ?
private function doPersist($entity, array &$visited)
{
if ($entity instanceof Subscription) exit(var_dump($entity));
$oid = spl_object_hash($entity);
if (isset($visited[$oid])) {
return; // Prevent infinite recursion
}
}
DateTime are object after all, and are always passed by reference. So, I strongly suggest you to edit your code like this:
$period = $payment->getDetail('period');
$validDate = clone $subscription->getPaymentValidUntil();
$validDate->add(new \DateInterval($period == Subscription::MONTH ? 'P1M' : 'P1Y'));
$subscription->setPaymentValidUntil($validDate);
$this->em->persist($subscription);
$this->em->flush();
This way, you ensure that you're passing a different object to the entity, and avoid any subtle bug in the EntityManager.