take a look at this table first then i'll explain.
parent {
id:number;
name:string;
}
child {
id:number;
name:string;
parent_id:number; //foreignKey to parent table
}
and this is the API request to do an update to backend
Request = {
id:1; //parent id
name:'update the name';
childs:[
{
id:1,
name:'child 1'
},
{
id:null, //if null its mean i have to create this child
name:'child 2'
}
]
}
so if the Request.childs have id on each item then i have to update the child, but if its doesn't have an id i have to create a new child.
And if child doesn't exist in Request.childs i have to delete it.
my question is how do i delete the previous child that doesn't exist in Request.childs ?
what i am doing now is i am deleting all the child that belongs to the parent_id and create a new child based on Request, but since child have soft delete on all the deleted child will get stacked in the database.
what currently i am doing
{
child::where('parent_id',Request->id)->delete();
foreach($Request->childs as $item){
child::create['name'=>$item->name,'parent_id'=>$Request->id];
}
}
what i probably i have to do
foreach($Request->childs as $item){
if($item->id == null){
child::create['name'=>$item->name,'parent_id'=>$Request->id];
}
child::find($item->id)->update['name'=>$item->name];
}
i just don't know how to delete the previous child record without deleting everything ?
This is the way I would do it:
public function patch(Request $request)
{
Child::whereNotIn('id', array_column($request->childs, 'id'))->delete();
Child::upsert([
$request->childs
], ['id'], ['name']);
}
If you don't want to soft delete the child you can apply forceDelete()
as below:
child::where('parent_id',Request->id)->forceDelete();
foreach($Request->childs as $item) {
if($item->id == null){
child::create['name'=>$item->name,'parent_id'=>$Request->id];
}
child::find($item->id)->update['name'=>$item->name];
}
Oh, it's big problem. I solved it for my project, but not very happy with this solution.
What you get
$parent->childs()->sync($request['childs'])
if you pass true as second value - null childs will be removed
How you can get it
Add this code to AppServiceProvider::boot() - it's not my code, just copy pasted from somewhere and slightly improved
HasMany::macro('sync', function (array $data, $deleting = true) {
/** #var HasMany $this */
$changes = [
'created' => [], 'deleted' => [], 'updated' => [],
];
$relatedKeyName = $this->getRelated()->getKeyName();
$current = $this->newQuery()->pluck($relatedKeyName)->all();
$castKey = function ($value) {
if (is_null($value)) {
return $value;
}
return is_numeric($value) ? (int) $value : (string) $value;
};
$castKeys = function ($keys) use ($castKey) {
return (array) array_map(function ($key) use ($castKey) {
return $castKey($key);
}, $keys);
};
$deletedKeys = array_diff($current, $castKeys(
Arr::pluck($data, $relatedKeyName))
);
if ($deleting && count($deletedKeys) > 0) {
$this->getRelated()->destroy($deletedKeys);
$changes['deleted'] = $deletedKeys;
}
$newRows = Arr::where($data, function ($row) use ($relatedKeyName) {
return Arr::get($row, $relatedKeyName) === null;
});
$updatedRows = Arr::where($data, function ($row) use ($relatedKeyName) {
return Arr::get($row, $relatedKeyName) !== null;
});
if (count($newRows) > 0) {
$newRecords = $this->createMany($newRows);
$changes['created'] = $castKeys(
$newRecords->pluck($relatedKeyName)->toArray()
);
}
foreach ($updatedRows as $row) {
$this->getRelated()->where($relatedKeyName, $castKey(Arr::get($row, $relatedKeyName)))
->update($row);
}
$changes['updated'] = $castKeys(Arr::pluck($updatedRows, $relatedKeyName));
return $changes;
});
You just need to get the ids from the database and compare
{
$childIds = child::where('parent_id',Request->id)->pluck('id')->toArray();
$flippedChildIds = array_flip($childIds);// we flip the keys and values for easy unsetting.
$childsToBeInsertedOrUpdated = [];
foreach($Request->childs as $item){
//add deleted at in case a child was deleted and need to be re activated
// if you dont want the child to be reactivated, remove "deleted_at" from both arrays
$childsToBeInsertedOrUpdated[] = ['id' => $item->id, 'name' => $item->name, 'deleted_at' => null];
//if exists, remove from the to be deleted childs
unset($flippedChildIds[$item->id]));
}
//only two request for delete and update/create (faster)
if ($flippedChildIds) {
Child::whereIn('id', array_keys($flippedChildIds))->delete();
}
if ($childsToBeInsertedOrUpdated) {
Child::upsert($childsToBeInsertedOrUpdated, ['id'], ['name', 'deleted_at']);
}
}
Related
Hello i have 2 tables that i want to call right now, for the EDIT (part of the CRUD)
tables:
table_a
table_b
i found in youtube how to update/edit from 2 tables, i need to call bot of the tables.
here's the code for the model
public function edit_this($ID_A)
{
return $this->db->table('table_a', '*i don't know how to insert the 2nd table')->where('ID_A', $ID_A)->get()->getRowArray();
}
Here's the controller
public function this_edit($ID_A)
{
$data = [
'title' => 'Admin',
'navbartitel' => 'You know this',
'alledit' => $this->theModel->edit_this($ID_A),
'validation' => \Config\Services::validation()
];
return view('this/all/edit', $data);
}
it works but i only can accsess the tabel_a, but i need them both so i can show what i've written in the edit form, from the database
anyone can help? thank you
$this->db->table(...) returns an instance of QueryBuilder and will happily accept a single string of comma-separated tables ("table1, table2..."), or even an array for that matter (['table1', 'table2'...]), as its first parameter. You are doing neither and instead passing multiple parameters.
When you call table(), the value passed in the first parameter is used during the creation of the database-specific Builder class:
public function table($tableName)
{
if (empty($tableName))
{
throw new DatabaseException('You must set the database table to be used with your query.');
}
$className = str_replace('Connection', 'Builder', get_class($this));
return new $className($tableName, $this);
}
The DB-specific Builder class has no constructor of its own so falls back on the __construct defined in BaseBuilder, which it extends:
public function __construct($tableName, ConnectionInterface &$db, array $options = null)
{
if (empty($tableName))
{
throw new DatabaseException('A table must be specified when creating a new Query Builder.');
}
$this->db = $db;
$this->from($tableName);
...
I've truncated this for brevity because the important part is that call to $this->from, which is in the end how multiple tables get processed:
public function from($from, bool $overwrite = false)
{
if ($overwrite === true)
{
$this->QBFrom = [];
$this->db->setAliasedTables([]);
}
foreach ((array) $from as $val)
{
if (strpos($val, ',') !== false)
{
foreach (explode(',', $val) as $v)
{
$v = trim($v);
$this->trackAliases($v);
$this->QBFrom[] = $v = $this->db->protectIdentifiers($v, true, null, false);
}
}
else
{
$val = trim($val);
// Extract any aliases that might exist. We use this information
// in the protectIdentifiers to know whether to add a table prefix
$this->trackAliases($val);
$this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
}
}
return $this;
}
I am returning JSON to my frontend like this:
public function newFlavorOrders()
{
$orders = request()->user()->ordersPaid;
return response()->json(['flavor_orders' => $orders]);
}
and right now, that returns this to the frontend:
{ orders: [
{
color: "Green"
size: "Large",
order_products: [ {'itemNum': 3, 'imgUrl': "zera.jpg"}, {'itemNum': 5, 'imgUrl': "murto.jpg"} ]
},
{
color: "Blue"
size: "Large",
order_products: [ {'itemNum': 3, 'imgUrl': "mcue.jpg"}, {'itemNum': 5, 'imgUrl': "cloa.jpg"} ]
}
]
}
But I want to alter the controller PHP function to add a field to each order_products item. I have the imgURL, but I want to add a processedImgUrl and stub it with true right now. How can I add the field to the above php function when returning the JSON?
Without the dataset this may not be exactly accurate but the way to do this is either to perform an array push or do a foreach loop and add create the index to be appended.
For example:
public function newFlavorOrders()
{
// CREATE A NEW ARRAY TO ADD THE MODIFIED DATA TO
$modified = array();
$orders = request()->user()->ordersPaid;
// LOOP THROUGH AND ADD THE VALUE TO THE ITERATION
foreach($orders as $row) {
foreach($row['order_products'] as $val){
$modified = $val;
if(!empty($val['imgUrl'])){
$modified['processedImgUrl'] = TRUE;
} else {
$modified['processedImgUrl'] = FALSE;
}
}
}
return response()->json(['flavor_orders' => $modified]);
}
Something like this should work. You need to loop through the first array, then get down to the next level array (order_products).
public function newFlavorOrders()
{
$orders = request()->user()->ordersPaid;
$orders = $orders->map(function ($order) {
$order->order_products = $order->order_products->map(function ($products) {
$products['processedImgUrl'] = true;
return $products;
});
return $order;
});
return response()->json(['flavor_orders' => $orders]);
}
I'm wondering what the best approach is to control which model attributes a given user is allowed to view.
To control which attributes they are allowed to modify I'm of course using scenarios, but sometimes they should be allowed to view attributes which they are not allowed to modify, so I can't just use the same list of attributes.
I want to control it at a central point, so preferably within the model I would guess.
What is the best way, or Yii intended method, to approach this?
I was thinking that I needed something similar to scenarios so, building on that idea I have now tried to make a solution where I create a method called viewable on my model, which returns a list of attributes that should be visible for the current scenario of the model. For example:
public function viewable() {
$scenario = $this->getScenario();
if ($scenario == self::SCENARIO_DEFAULT) {
return [];
} elseif ($scenario == self::SCENARIO_EV_ADMIN) {
return $this->attributes(); //admin is allowed to see all attributes on the model
} elseif ($scenario == self::SCENARIO_EV_ORGANIZER_INSERT || $scenario == self::SCENARIO_EV_ORGANIZER_UPDATE) {
$attributes = $this->activeAttributes(); //use list of attributes they can edit as the basis for attributes they can view
array_push($attributes, 'ev_approved', 'ev_status'); //add a few more they are allowed to view
return $attributes;
} else {
return [];
}
}
Then eg. in GridView or DetailView I pass the list of columns/attributes through a helper that will filter out any attributes that were not returned by viewable. Eg.:
'attributes' => MyHelper::filterAttributes([
'eventID',
[
'attribute' => 'organizerID',
'value' => \app\models\Organizer::findOne($model->organizerID)['org_name'],
],
'ev_name',
....
], $model->viewable()),
My helper method being like this:
public static function filterAttributes($all_attributes, $attributes_to_keep) {
$output = [];
foreach ($all_attributes as $value) {
if (is_string($value)) {
$colon = strpos($value, ':');
if ($colon === false) {
$name = $value;
} else {
$name = substr($value, 0, $colon);
}
} elseif (is_array($value)) {
if ($value['attribute']) {
$name = $value['attribute'];
} elseif ($value['class']) {
// always leave special entries intact (eg. class=yii\grid\ActionColumn)
$output[] = $value;
continue;
} else {
new UserException('Attributes name not found when filtering attributes.');
}
} else {
new UserException('Invalid value for filtering attributes.');
}
if (in_array($name, $attributes_to_keep)) {
$output[] = $value;
}
}
return $output;
}
And in create.php/update.php (or _form.php actually) I do this:
$editAttribs = $model->activeAttributes();
$viewAttribs = $model->viewable();
....
if (in_array('organizerID', $viewAttribs)) {
echo $form->field($model, 'organizerID')->textInput(['disabled' => !in_array('organizerID', $editAttribs) ]);
}
....
Feedback is welcome!
I have a database with a couple of tables related between them. For example, the table User contains all the users in the system.
Then I have an index table named User_friend with the relation between a user an it's friends.
I have a function loadObject($class, $id) which is called like:
loadObject('User', 1);
and returns the User with id = 1 as an array with the following format:
array(
'id' => 1,
'username' => 'My user',
// the following array contains all the entries in User_invited
'friends' => [2, 3, 4, 5],
// same for comments
'comments' => [6, 7]
'type' => 'User'
);
I'm trying to come up with a recursive function that checks the User with id = 1, finds all the friends (inside the 'friends' array) and then loops through each value, find those Users and it's friends until it reaches the end of the chain without duplicating any entries.
This seems pretty straight forward. The problem is that apart from friends we can have other relations with Comments, Events and many other tables.
The tricky part is that this function should not only work with the 'User' class, but also with any class we define.
What I'm doing is using some sort of Indexed array to define which index tables refer to which main tables.
For example:
$dependencies = [
'friends' => 'User'
];
This means that, when we find the 'friends' key, we should query the 'User' table.
Here's my code:
<?php
$class = $_GET['class'];
// if we receive a collection of ids, find each individual object
$ids = explode(",", $_GET['ids']);
// load all main objects first
foreach($ids as $id) {
$error = isNumeric($id);
$results[] = loadObject($class,$id);
}
$preload = $results;
$output = [];
$output = checkPreload($preload);
print json_encode($output);
function checkPreload($preload)
{
$dependencies = [
'comment' => 'Comment',
'friend' => 'User',
'enemy' => 'User',
'google' => 'GoogleCalendarService',
'ical' => 'ICalCalendarService',
'owner' => 'User',
'invited' => 'User'
];
foreach($preload as $key => $object)
{
foreach($object as $property => $values)
{
// if the property is an array (has dependencies)
// i.e. event: [1, 2, 3]
if(is_array($values) && count($values) > 0)
{
// and if the dependency exists in our $dependencies array, find
// the next Object we have to retrieve
// i.e. event => CatchAppCalendarEvent
if(array_key_exists($property, $dependencies))
{
$dependentTable = $dependencies[$property];
// find all the ids inside that array of dependencies
// i.e. event: [1, 2, 3]
// and for each ID load th the object:
// i.e. CatchAppCalendarEvent.id = 1, CatchAppCalendarEvent.id = 2, CatchAppCalendarEvent.id = 3
foreach($values as $id)
{
$dependantObject = loadObject($dependencies[$property], $id);
// if the object doesn't exist in our $preload array, add it and call the
// function again
if(!objectDoesntExist($preload, $dependantObject)) {
$preload[] = $dependantObject;
reset($preload);
checkPreload($preload);
}
}
}
}
}
}
return $preload;
}
// 'id' and 'type' together are unique for each entry in the database
function objectDoesntExist($preload, $object)
{
foreach($preload as $element)
{
if($element['type'] == $object['type'] && $element['id'] == $object['id']) {
return true;
}
}
return false;
}
I'm pretty sure I'm close to the solution but I'm not able to understand why is not working. Seems to get stuck in an infinite loop even if I'm using a function to check if the object has been inserted in the $preload array. Also, sometimes doesn't check the next set of elements. Could it be because I'm appending the data to the $preload variable?
Any help is more than welcome. I've been trying to find algorithms for resolving dependencies but nothing applied to MySQL databases.
Thanks
After some failed tests I've decided to not use a recursive approach but an iterative approach.
What I'm doing is start with one element and put it in a "queue" (an array), find the dependencies for that element, append them to the "queue" and then step back and re-check the same element to see if there are any more dependencies.
The function to check the dependencies is a bit different now:
/**
* This is the code function of our DRA. This function contains an array of dependencies where the keys are the
* keys of the object i.e. User.id, User.type, etc. and the values are the dependent classes (tables). The idea
* is to iterate through this array in our queue of objects. If we find a property in one object that that matches
* the key, we go to the appropriate class/table (value) to find more dependencies (loadObject2 injects the dependency
* with it's subsequent dependencies)
*
*/
function findAllDependenciesFor($element)
{
$fields = [
'property' => 'tableName',
...
];
$output = [];
foreach($element as $key => $val) {
if(array_key_exists($key, $fields)) {
if(is_array($val)) {
foreach($val as $id) {
$newElement = loadObject($fields[$key], $id);
$output[] = $newElement;
}
}
else {
// there's been a field conversion at some point in the app and some 'location'
// columns contain text and numbers (i.e. 'unknown'). Let's force all values to be
// and integer and avoid loading 0 values.
$val = (int) $val;
if($val != 0) {
$newElement = loadObject($fields[$key], $val);
$output[] = $newElement;
}
}
}
}
return $output;
}
I'm also using the same function as before to check if the "queue" already contains that element (I have renamed the function to be "objectExists" instead of "objectDoesntExist". As you can see I check the type (table) and the id because the combination of these two properties is unique for the whole system/database.
function objectExists($object, $queue)
{
foreach($queue as $element) {
if($object['type'] == $element['type'] && $object['id'] == $element['id']) {
return true;
}
}
return false;
}
Finally, the main function:
// load all main objects first
foreach($ids as $id) {
$error = isNumeric($id);
$results[] = loadObject($class,$id);
}
$queue = $results;
for($i = 0; $i < count($queue); $i++)
{
// find all dependencies of element
$newElements = findAllDependenciesFor($queue[$i]);
foreach($newElements as $object) {
if(!objectExists($object, $queue)) {
$queue[] = $object;
// instead of skipping to the next object in queue, we have to re-check
// the same object again because is possible that it included new dependencies
// so let's step back on to re-check the object
$i--;
}
}
$i++;
}
As you can see, I'm using a regular "for" instead of a "foreach". This is because I need to be able to step forward/backward in my "queue".
I need to return family data (parents, siblings and partners) for 'x' number of generations (passed as $generations parameter) starting from a single person (passed as $id parameter). I can't assume two parents, this particular genealogy model has to allow for a dynamic number of parents (to allow for biological and adoptive relationships). I think my recursion is backwards, but I can't figure out how.
The code below is triggering my base clause 5 times, once for each generation, because $generation is being reduced by 1 not for every SET of parents but for every parent. What I want is for the base clause ($generations == 0) to only be triggered once, when 'x' number of generations for all parents of the initial person are fetched.
public function fetchRelationships($id = 1, $generations = 5, $relationships = array())
{
$perId = $id;
if ($generations == 0) {
return $relationships;
} else {
$parents = $this->fetchParents($perId);
$relationships[$perId]['parents'] = $parents;
$relationships[$perId]['partners'] = $this->fetchPartners($perId);
if (!empty($parents)) {
--$generations;
foreach ($parents as $parentRel) {
$parent = $parentRel->getPer2();
$pid = $parent->getId();
$relationships[$perId]['siblings'][$pid] = $this->fetchSiblings($perId, $pid);
$perId = $pid;
$relationships[$perId] = $this->fetchRelationships($perId, $generations, $relationships);
}
}
return $relationships;
}
}
The methods fetchPartners, fetchParents and fetchSiblings just fetch the matching entities. So I am not pasting them here. Assuming that there are 2 parents, 5 generations and each generation has 2 parents then the return array should contain 62 elements, and should only trigger the base clause once those 62 elements are filled.
Thanks, in advance, for any help.
-----------Edit--------
Have rewritten with fetchSiblings and fetchPartners code removed to make it easier to read:
public function fetchRelationships($id = 1, $generations = 5, $relationships = array())
{
$perId = $id;
if ($generations == 0) {
return $relationships;
} else {
$parents = $this->fetchParents($perId);
$relationships[$perId]['parents'] = $parents;
if (!empty($parents)) {
--$generations;
foreach ($parents as $parentRel) {
$perId = $parentRel->getPer2()->getId();
$relationships[$perId] = $this->fetchRelationships($perId, $generations, $relationships);
}
}
return $relationships;
}
}
Garr Godfrey got it right. $generations will equal zero when it reaches the end of each branch. So you'll hit the "base clause" as many times as there are branches. in the foreach ($parents as $parentRel) loop, you call fetchRelationships for each parent. That's two branches, so you'll have two calls to the "base clause". Then for each of their parents, you'll have another two calls to the "base clause", and so on...
Also, you're passing back and forth the relationships, making elements of it refer back to itself. I realize you're just trying to retain information as you go, but you're actually creating lots of needless self-references.
Try this
public function fetchRelationships($id = 1, $generations = 5)
{
$perId = $id;
$relationships = array();
if ($generations == 0) {
return $relationships;
} else {
$parents = $this->fetchParents($perId);
$relationships[$perId]['parents'] = $parents;
if (!empty($parents)) {
--$generations;
foreach ($parents as $parentRel) {
$perId = $parentRel->getPer2()->getId();
$relationships[$perId] = $this->fetchRelationships($perId, $generations);
}
}
return $relationships;
}
}
you'll still hit the base clause multiple times, but that shouldn't matter.
you might be thinking "but then i will lose some of the data in $relationships", but you won't. It's all there from the recursive returns.
If you're pulling this out of a database, have you considered having the query do all of the leg work for you?
Not sure how you need the data stacked or excluded, but here's one way to do it:
<?php
class TreeMember {
public $id;
// All three should return something like:
// array( $id1 => $obj1, $id2 => $obj2 )
// and would be based on $this->$id
public function fetchParents(){ return array(); }
public function fetchPartners(){ return array(); };
public function fetchSiblings(){ return array(); };
public function fetchRelationships($generations = 5)
{
// If no more to go
if ($generations == 0) { return; }
$branch = array();
$branch['parents'] = $this->fetchParents();
$branch['partners'] = $this->fetchPartners();
$branch['partners'] = $this->fetchSiblings();
// Logic
$generations--;
foreach($branch as $tmType, $tmArr)
{
foreach($tmArr as $tmId => $tmObj)
{
$branch[$tmType][$tmId] =
$mObj->fetchRelationships
(
$generations
)
);
});
return array($this->id => $branch);
}
}