I have a classified with categories. My classified can have one or several categories:
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="classified", cascade={"persist"}, orphanRemoval=true)
*/
protected $categories;
An important thing to know is that my Category entity has composite primary keys : the ID of the category, and the classified.
I want to populate categories into my classified with a choices field:
$builder->add(
$builder->create('categories', ChoiceType::class, [
'choices' => [
'Category 1' => 1,
'Category 2' => 2,
'Category 3' => 3,
'Category 4' => 4,
],
'multiple' => true,
])->addModelTransformer(new CallbackTransformer(
// Transform collection of entities into an array of IDs
function($categories) {
if (!$categories) {
return [];
}
return $categories->map(function(Category $category) {
return $category->getId();
})->toArray();
},
// Reverse transform IDs array from request to a collection of entities
function($categories) {
$items = [];
if ($categories && is_array($categories)) {
foreach ($categories as $id) {
$items[] = new Category($id);
}
}
return $items;
}
))
);
As you can see I have a transformer to convert the categories collection to a list of ID, and vice versa.
Everything is fine during the creation of my classified, but edition is bugged.
My problem is that when I submit the form, I have duplicate entries issues:
An exception occurred while executing 'INSERT INTO tv_classified_category (id, classified_id) VALUES (?, ?)' with params [4, 1]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '4-1' for key 'PRIMARY'
Here is what I found when I dump the list of categories before and after handleRequest:
dump($classified->getCategories()->toArray());
$form->handleRequest($request);
if ($form->isValid()) {
dump($classified->getCategories()->toArray());
Debug result:
array:2 [▼
0 => Category {#1036 ▶}
1 => Category {#1038 ▶}
]
array:3 [▼
2 => Category {#4108 ▶}
3 => Category {#4110 ▶}
]
As you can see, there is a strange mismatch between keys, and the PHP ids of objects are differents before and after the handleRequest call. What is wrong and how can I fix this?
Regards,
So I found that the problem is that when an item is removed then added again in the collection, it causes a duplicate entry because Doctrine performs inserts before deletions.
To fix my problem, I need to make sure that same Category objects are used if my categories already in the classified. The only way I found to do this is some uggly code that will probably be broken one day. I hope anybody will find a better way to do this than the code bellow.
// Add the listener on the categories field (a ChoiceType field)
->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
$form = $event->getForm();
$categories = $form->getData();
$classified = $form->getParent()->getData();
$classifiedCategories = [];
foreach ($classified->getCategories() as $classifiedCategory) {
$classifiedCategories[$classifiedCategory->getId()] = $classifiedCategory;
}
$modelData = new ArrayCollection();
foreach ($categories as $category) {
$modelData->add($classifiedCategories[$category->getId()] ?? $category);
}
// This is uggly, don't reproduce this at home kids :)
$refProperty = new \ReflectionProperty($form, 'modelData');
$refProperty->setAccessible(true);
$refProperty->setValue($form, $modelData);
})
I need to do this stuff in POST_SUBMIT (and use Reflection) because in SUBMIT I only get the flat view data and not the ArrayCollection.
Related
In my project, all users are stored in the user table. The ones who are suppliers, their user key goes into the supplier table.
Each product can have multiple variations. The key product goes into the productattribute table.
Each supplier can have multiple products with multiple attributes.
So there is a many to many table of product attributes & suppliers named productsupplier.
I want to give the option to the user to make a new Purchase Order.
The Purchase Order will be for the supplier.
The user will select the supplier and then all the products that are assigned to him will show up.
The problem is that a product with the same name can have different attributes. Therefore, when I write the code for the products belonging to a supplier to show up, one product (which has different variations) shows up multiple times.
I have been struggling to use distinct or unique functions to show the name of one product once only, but since it's in a foreach loop, it doesn't work.
Here's the code:
public function new_po_next(Request $request)
{
$product = array();
$supplier = \App\supplier::where('user_id', $request->supplier)->first();
$productsupplier = \App\productsupplier::where('supplier_id', $supplier->id)->get();
foreach ($productsupplier as $ps) {
$productattribute = \App\productattribute::where('id', $ps->productattribute_id)->first();
$prod = \App\product::where('id', $productattribute->product_id)->first();
$product [] = [
'prod_supp_id' => $ps->id,
'prod_supp_prodattid' => $ps->productattribute_id,
'prod_supp_cost' => $ps->cost,
'prod_att_id' => $productattribute->id,
'prod_att_color' => $productattribute->color_id,
'prod_att_size' => $productattribute->size_id,
'prod_att_material' => $productattribute->material_id,
'prod_att_prodid' => $productattribute->product_id,
'prod_id'=>$prod->id,
'prod_name' =>$prod->name,
];
}
$status = $request->status;
$deldate = $request->deldate;
$discount = $request->discount;
return view("vendor.new-po-next")
->with('status', $status)
->with('deldate', $deldate)
->with('discount', $discount)
->with('product', $product);
}
This code is giving the following result (it's the correct result as product1 has 1 variation only but product5 has 3 variations).
What I want is that it shows 'product1' and 'product5' (product5 ONCE only).
If the user selects product5, then it shows the rest of the variations to select from.
Please let me know how it can be done!
How about using Laravel collections, you can convert your array to collection,
// After the loop is finished.
$productCollection = collect($product);
$groupedProductCollection = $collection->groupBy('prod_name');
$groupedProduct = $groupedProductCollection->all(); // all method will convert it back to array
// then return the value
return view("vendor.new-po-next")
->with('status', $status)
->with('deldate', $deldate)
->with('discount', $discount)
->with('product', $groupedProduct);
Working
Lets say your product array is like this
$product = [
['prod_name' => 'product5', 'prod_supp_cost' => '$4'],
['prod_name' => 'product5', 'prod_supp_cost' => '$3'],
['prod_name' => 'product1', 'prod_supp_cost' => '$6'],
]
Then after groupby it will be like,
[
'product5' => [
['prod_name' => 'product5', 'prod_supp_cost' => '$4'],
['prod_name' => 'product5', 'prod_supp_cost' => '$3'],
],
'product1' => [
['prod_name' => 'product1', 'prod_supp_cost' => '$6'],
],
]
Then you you show the array_keys in list and its values separately when the user clicks on it.
EDIT: How to use it in frontend
<select id="" name="">
#foreach(array_keys($groupedProduct) as $product_names)
<option value="{{ $product_names }}"></option>
#endforeach
</select>
If someone clicks then show it using $groupedProduct[$product_names]
Though the long term and best solution as suggested by #miken32 would be to use eloquent relationships & let relations handle all the querying so that there is no need for a for loop.
I have an array with orders. Example:
$orders = [
'0' => [
'ordernumber' => 1,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
],
'1' => [
'ordernumber' => 2,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
],
'2' => [
'ordernumber' => 3,
'customer' => [
'phone' => '0987654321',
'mobile' => '0687654321'
],
],
'3' => [
'ordernumber' => 3,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
]
];
I want to sort these orders. As you can see there can be orders where the same customer (customer with same phone number, this can be either same phone number or same mobile number) has multiple orders. I want to put all the orders that have the same phone number (doesn't matter if the phone number matches or the mobile number) in an array $duplicateOrders and all the "single" orders (orders that dont match a phone number) in an array $singleOrders. At the end the orders array must be empty. But no order can be lost or be in both arrays.
I have tried to loop through the orders with a foreach loop where I put every order in the $singleOrders array and unset it from the $orders array. I than try to match that order with another foreach loop to all the remaining orders in $orders. If I get a match i put that order (this is done once) in the $duplicateOrders array and every match of it also (I unset every match also from the $orders array). If the orders array is empty I stop, otherwise the first foreach loops kicks in and takes the next order and the proces repeats. This is my code:
protected function splitDuplicateOrders()
{
$singleOrderKey = 0;
if ($this->orders) {
foreach ($this->orders as $key => $order) {
if (count($this->orders) == 0) {
break;
}
array_push($this->singleOrders, $order);
unset($this->orders[$key]);
$orderPushed = false;
foreach ($this->orders as $otherKey => $value) {
if ($order->customer->phone == $value->customer->phone || $order->customer->mobile == $value->customer->mobile) {
if (!$orderPushed) {
array_push($this->duplicateOrders, $order);
}
array_push($this->duplicateOrders, $value);
unset($this->orders[$otherKey]);
unset($this->singleOrders[$singleOrderKey]);
$orderPushed = true;
}
}
$singleOrderKey++;
}
}
}
I expected to have an $duplicateOrders array with all the duplicates and a $singleOrders array with all the singles. I tested this with an $orders array of a total of 4 orders where 2 of them were duplicates and 2 were singles. The function sorted it nicely (but only if the orders aren't right after each other, if they are it still sorts the duplicates right but leaves one also in the $singleOrders and than I have 5 orders). Than I tested it where there were 3 duplicates and 1 single order. The $duplicateOrders array was correct but in the $singleOrders the single order was placed but also one duplicate order from the $orders array. It somehow removed the 2 duplicates correct but left one duplicate in the $singleOrders array.
Can someone help me to debug this or provide a different approach? I have been trying to solve this for 2 days but no success.
You could make use of Laravel Collections, in this case I'm gonna use the partition() method. From the documentation:
partition()
The partition method may be combined with the list PHP function to
separate elements that pass a given truth test from those that do not:
$collection = collect([1, 2, 3, 4, 5, 6]);
list($underThree, $equalOrAboveThree) = $collection->partition(function ($i) {
return $i < 3;
});
$underThree->all();
// [1, 2]
$equalOrAboveThree->all();
// [3, 4, 5, 6]
So in your case:
$orders = /** query or API to get orders as array */;
list($repeated, $single) = collect($orders)
->partition(function ($order) use ($orders) {
return $orders->where('customer.phone', $order['customer']['phone'])->count() > 1
OR $orders->where('customer.mobile', $order['customer']['mobile'])->count() > 1;
});
// now you can use them:
$repeated->all();
$single->all();
Notice that this two newly created objects ($repeated and $single) are in fact also instances of the Collection class (as you can see where I used the all() method on each), so you can keep constraining/sorting/customizing them with the help of the Collection's methods.
I have 3 models:
Match Team Player
And i want to create a table with the following structure:
id | match_id | team_id | player_id
So that i can associate the 3 models i refered.
I created a 4th model MatchPlayers for the table i referred and I can use the 'search' functions without a problem. Like this:
$match->matchPlayers()->first()->team()->get()
And it returns the excpected result, but I cant do a
$match->matchPlayers()->sync([])
So, how should i solve this? Is my relationship wrong or the sync method isnt allowed on a 3 model relationship and I shoud use other method?
Thanks in advance
Edit:
Match.php
public function teamPlayers(){
return $this->hasMany('\Modules\Matchs\Entities\MatchPlayer');
}
Team.php
public function matchTeamPlayers(){
return $this->hasMany('\Modules\Matchs\Entities\MatchPlayer');
}
Player.php
public function matchTeamPlayers(){
return $this->hasMany('\Modules\Matchs\Entities\MatchPlayer');
}
MatchPlayer.php
public function player(){
return $this->belongsTo('\Modules\Players\Entities\Player');
}
public function match(){
return $this->belongsTo('\Modules\Matchs\Entities\Match');
}
public function team(){
return $this->belongsTo('\Modules\Teams\Entities\Team');
}
If you've followed the Laravel documentation on Pivot tables and Many-Many relationships found here, and it's still not working, you might have more luck with "Attach". For example;
$matchPlayer = MatchPlayer::create([...]);
$match->matchPlayers()->attach($matchPlayer)
A good example of sync vs attach can be found here
Using a fourth model for this kind of relationship makes sense, as it gives you a navigation property for the third relation on your pivot table. This way you can form more complex queries this way.
For your particular problem, syncing based on match_id and team_id, I would simply do something like this:
$matchId = 123;
$teamId = 234;
$rows = [
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 345],
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 346],
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 347],
];
// remove all previously stored connections
MatchPlayer::where('match_id', $matchId)
->where('team_id', $teamId)
->delete();
// insert the news ones
// (you could also use MatchPlayer::create() per item or
// $matchPlayer->save(), it doesn't matter)
MatchPlayer::insert($rows);
If this operation occurs very frequently, you will potentially burn through a lot of id values of the pivot table. In this case you could also perform a more efficient sync, which is slightly more complex:
$matchId = 123;
$teamId = 234;
$rows = [
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 345],
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 346],
['match_id' => $matchId, 'team_id' => $teamId, 'player_id' => 347],
];
// delete all players that are not among the new data anymore
MatchPlayer::where('match_id', $matchId)
->where('team_id', $teamId)
->whereNotIn('player_id', array_pluck($rows, 'player_id'))
->delete();
// remove rows from new data that already exist
$exist = MatchPlayer::where('match_id', $matchId)
->where('team_id', $teamId)
->pluck('player_id')
->toArray();
$rows = array_filter($rows, function ($value, $key) use ($exist) {
return ! in_array($value['player_id'], $exist);
});
// then we store the remaining data
MatchPlayer::insert($rows);
I'm having trouble updating and creating related records depending on if they exist or not. I want to update the ingredient if they exist, if not insert the ingredient into the database and relate it to the current meal.
public function update($id)
{
$meal = Meal::find($id);
$meal->name = Input::get('name');
// create ingredients
$ingredients = Input::get('ingredient');
$meal_ingredients = array();
foreach($ingredients as $ingredient)
{
$meal_ingredients[] = new Ingredient(array(
'name' => $ingredient['name'],
'unit' => $ingredient['unit'],
'quantity' => $ingredient['quantity']
));
}
//save into the DB
$meal->save();
$meal->ingredients()->saveMany($meal_ingredients);
// redirect
Flash::success('Votre repas a bien été mis à jour!');
return Redirect::to('/meals');
}
Step 1 : Get Meal
$meal = Meal::find($id);
Step 2 : Get Ingredients of Meal (create relation for this)
$ingredients = $meal->ingredients;
Step 3 : Compare Input to current and add does not exist
$new_ingredients = array();
foreach($ingredients as $ingredient)
{
if(!in_array($ingredient->toArray(), Input::get('ingredient')) {
$new_ingredients[] = new Ingredient(array(
'name' => $ingredient['name'],
'unit' => $ingredient['unit'],
'quantity' => $ingredient['quantity']
));
}
}
Step 4 Update
$meal->ingredients->saveMany($new_ingredients);
Ensure you got the relation between meal and ingredient correctly
You can use the firstOrNew() method. You pass it an array of data, and it will return the first record that matches that data, or if no record is found, a new instance of the class with the searched fields already set.
foreach($ingredients as $ingredient)
{
$meal_ingredients[] = Ingredient::firstOrNew(array(
'name' => $ingredient['name'],
))->fill(array(
'unit' => $ingredient['unit'],
'quantity' => $ingredient['quantity']
));
}
I have 2 tables
cars (id, title)
parts (id, title)
and I want to assosiate every car with a lot of parts and the price of this part for the specific car...
I believe the best way is to associate them through a table:
car_parts(id, car_id, part_id, price)
How do I define such a relation in Laravel's Eloquent?
I want to do
$car = Car::find(1);
$parts = $car->parts;
and I want to get an array of objects like so
{
'id' => 1,
'title' => 'rear flash',
'price' => '10.00',
},{
...
}
if I try the
public function parts(){
return $this->belongsToMany('Part', 'car_parts', 'car_id', 'part_id');
}
I dont get the price...
TIA
The price is available on the pivot model if you include it in withPivot:
public function parts ()
{
return $this->belongsToMany('Part', 'car_parts', 'car_id', 'part_id')
->withPivot('price');
}
Then you can map over the parts collection to get the arrays you want:
$parts = Car::find(1)->parts->map(function ($part)
{
return [
'id' => $part->id,
'title' => $part->title,
'car_id' => $part->pivot->car_id,
'part_id' => $part->pivot->part_id,
'price' => $part->pivot->price,
];
});