In my database, the user management is divided into two tables:
- One created by the symfony sfDoctrineGuard plugin (sfGuardUser), with the username, the password and other information used by the plugin
- another one that I created to extend this table with more properties such as the firstname, surname etc...
What I want to do is to gather all the properties of the two tables in a same object to display all the information related to any member on a specific page.
In that purpose I did a join of the two tables like this:
$q = $this->createQuery()
->from('sfGuardUser u')
->leftJoin('u.Mishmember m WITH u.id = ?', $rel['member_id']);
$member = $q->fetchOne();
My problem is that the generated query seems correct since it selects all the attributes of both tables, but in the $member variable, I can only access the properties of the sfGuardUser object.
I want the $member object to encapsulate all the properties of both tables/doctrine objects.
How would you do that?
EDIT1: if I do a
print_r($member->toArray())
after the previous code I get a 2 dimensional array containing all the properties of sfGuardUser in a first dimension and the properties of my second table in a second dimension. To be clear the result of print_r is like:
Array (
[table1-prop1] => value
[table1-prop2] => value
[table1-prop3] => value
[Table2] => Array (
[table2-prop1] => value
[table2-prop2] => value
[table2-prop3] => value
[table2-prop4] => value
)
)
So to access for example the table2-prop3 property I have to do:
$var[Table2][table2-prop3];
Which is not what I want because I want to consider all the properties as part as the sameobjet or array (as if there were only one table.)
If you're still with me the above array should look like:
Array (
[table1-prop1] => value
[table1-prop2] => value
[table1-prop3] => value
[table2-prop1] => value
[table2-prop2] => value
[table2-prop3] => value
[table2-prop4] => value
)
)
Hope that helps to understand my problem.
EDIT2 (answer to DuoSRX and DrColossos)
Thank you both for your interesting answers.
Well, the reason why I'd rather not to have a Mishmember property in my sfGuardUser is that both tables/classes stand for the same entity(the user). Though this fragmentation is inevitable in my database (it wouldn't be wise to edit directly the sfGuardPlugin to add my properties), I'd like that the code of the application would be as if I had one table, because that would be much more sensible and logical to use (imagine that another developer who doesn't know the model would have to work on the controller or the templates...)
What would you think of adding a Doctrine class User that inherits from sfGuardUser and Mishmember (is there multiple inheritance in PHP5?) so that my controller would have only one user class to deal with?
It would make much more sense to me to ask all the attributes of any user without bothering to find out in which table they are stored.
I'm not sure about how doctrine inheritance works but it seem the neatest solution to me (please tell me if I'm wrong!)
I'm not sure I've been clear enough so please don't hesitate to ask anything.
If you don't need objects but just arrays you can do :
$result = $query->fetchArray();
$mishmember = $result['Mishmember'];
unset($result['Mishmember']);
$user = array_merge($result, $mishmember);
That should do the trick, but I think this is too complex. What is the problem with having an multi-dimensional array ?
Edit:
Well then you could use simple or column_aggregation inheritance :
Mishmember:
inheritance:
type: simple (or column_aggregation)
extends: sfGuardUser
columns:
myfield:
type:integer
And then :
$mishmember = Doctrine::getTable('Mishmember')->find(1);
echo $mishmember->myfield;
See the doctrine documentation for more about inheritance.
This is not really how ORMs work: As you noted, you get a sfGuardUser Object/Array back. Now the Mishmember is nothing more than a property to sfGuardUser (it's an coincindent, that is is an object iteself). So instead of having just a e.g. string property, you have an object/array property.
edit: Of yourse you can merge/combine/re-create the array/object (as the other answer suggests), if you don't like the multi-dimensional aspect. But keep in mind, that these operations can get pretty complex when you have larger amounts of returned data.
Related
I assume that this should all be in one query in order to prevent duplicate data in the database. Is this correct?
How do I simplify this code into one Eloquent query?
$user = User::where( 'id', '=', $otherID )->first();
if( $user != null )
{
if( $user->requestReceived() )
accept_friend( $otherID );
else if( !$user->requestSent() )
{
$friend = new Friend;
$friend->user_1= $myID;
$friend->user_2 = $otherID;
$friend->accepted = 0;
$friend->save();
}
}
I assume that this should all be in one query in order to prevent
duplicate data in the database. Is this correct?
It's not correct. You prevent duplication by placing unique constraints on database level.
There's literally nothing you can do in php or any other language for that matter, that will prevent duplicates, if you don't have unique keys on your table(s). That's a simple fact, and if anyone tells you anything different - that person is blatantly wrong. I can explain why, but the explanation would be a lengthy one so I'll skip it.
Your code should be quite simple - just insert the data. Since it's not exactly clear how uniqueness is handled (it appears to be user_2, accepted, but there's an edge case), without a bit more data form you - it's not possible to suggest a complete solution.
You can always disregard what I wrote and try to go with suggested solutions, but they will fail miserably and you'll end up with duplicates.
I would say if there is a relationship between User and Friend you can simply employ Laravel's model relationship, such as:
$status = User::find($id)->friends()->updateOrCreate(['user_id' => $id], $attributes_to_update));
Thats what I would do to ensure that the new data is updated or a new one is created.
PS: I have used updateOrCreate() on Laravel 5.2.* only. And also it would be nice to actually do some check on user existence before updating else some errors might be thrown for null.
UPDATE
I'm not sure what to do. Could you explain a bit more what I should do? What about $attributes_to_update ?
Okay. Depending on what fields in the friends table marks the two friends, now using your example user_1 and user_2. By the example I gave, the $attributes_to_update would be (assuming otherID is the new friend's id):
$attributes_to_update = ['user_2' => otherID, 'accepted' => 0 ];
If your relationship between User and Friend is set properly, then the user_1 would already included in the insertion.
Furthermore,on this updateOrCreate function:
updateOrCreate($attributes_to_check, $attributes_to_update);
$attributes_to_check would mean those fields you want to check if they already exists before you create/update new one so if I want to ensure, the check is made when accepted is 0 then I can pass both say `['user_1' => 1, 'accepted' => 0]
Hope this is clearer now.
I'm assuming "friends" here represents a many-to-many relation between users. Apparently friend requests from one user (myID) to another (otherId).
You can represent that with Eloquent as:
class User extends Model
{
//...
public function friends()
{
return $this->belongsToMany(User::class, 'friends', 'myId', 'otherId')->withPivot('accepted');
}
}
That is, no need for Friend model.
Then, I think this is equivalent to what you want to accomplish (if not, please update with clarification):
$me = User::find($myId);
$me->friends()->syncWithoutDetaching([$otherId => ['accepted' => 0]]);
(accepted 0 or 1, according to your business logic).
This sync method prevents duplicate inserts, and updates or creates any row for the given pair of "myId - otherId". You can set any number of additional fields in the pivot table with this method.
However, I agree with #Mjh about setting unique constraints at database level as well.
For this kind of issue, First of all, you have to enjoy the code and database if you are working in laravel. For this first you create realtionship between both table friend and user in database as well as in Models . Also you have to use unique in database .
$data= array('accepted' => 0);
User::find($otherID)->friends()->updateOrCreate(['user_id', $otherID], $data));
This is query you can work with this . Also you can pass multiple condition here. Thanks
You can use firstOrCreate/ firstOrNew methods (https://laravel.com/docs/5.3/eloquent)
Example (from docs) :
// Retrieve the flight by the attributes, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// Retrieve the flight by the attributes, or instantiate a new instance...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
use `firstOrCreate' it will do same as you did manually.
Definition of FirstOrCreate copied from the Laravel Manual.
The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the given attributes.
So according to that you should try :
$user = User::where( 'id', '=', $otherID )->first();
$friend=Friend::firstOrCreate(['user_id' => $myId], ['user_2' => $otherId]);
It will check with both IDs if not exists then create record in friends table.
When I execute a query via Laravel's Eloquent ORM, I want to get the row ID as the Array result key, this will make things easier when I want to check something based on an ID (using array_key_exists) or do some look ups (if I need a specific entry from the result array)
Is there any way I can tell Eloquent to set the key to the fields ID?
You can simply do
Model::all()->keyBy('category_id');
Since you have an Eloquent Collection (a child class of the generic Collection class) you can use the getDictionary method. $collection->getDictionary() will give you an array of your Category objects keyed by their primary keys.
If you wanted another Collection rather than a native PHP array, you could instead use $collection->keyBy($property). It looks like your primary key is category_id, so $collection->keyBy('category_id'). You can use that method to key by any arbitrary property, including any get mutators you may have written.
While getDictionary is unique to the Eloquent Collection extension, keyBy is available to all Laravel Collection objects. See the Laravel 4.2 API docs or Laravel 5.0 API docs.
You have a Support\Collection/Database\Eloquent\Collection you can use the method lists('id') to return an array of the id of each of the models within the collection.
Then use array_combine to map the keys to the models. The result of which will be an array with the id mapped to their corresponding model.
If you need id as the key, then rely on the Collection:
$collection = Model::all();
$collection->getDictionary();
// returns:
array(
1 => object(Model) ( ... ),
2 => object(Model) ( ... ),
...
idN => object(Model) ( ... )
);
Otherwise, nice or not, you can do this:
$keys = $collection->lists('columnName');
$arrayOfModels = array_combine($keys, $collection->getDictionary);
pluck('label','id') on the get() output is what you're looking for in Laravel 5.8+, which gives you a collection, ready to be toArray()ed
eg:
$options = \App\DataChoice::where([ 'field_id' => $dataField->id , 'active' => true ])->get()->pluck('label','id')->toArray();
I had a similar challenge - I wanted to a PHP array to allow me to create a with an for each choice, with the 's value being the id auto increment column, and the displayed text being the value.
I started using Laravel yesterday, the ORM seems powerful. Does it have any way of updating rows in related models? This is what I tried:
Step 1: Generate a JSON object with the exact structure the database has. The JSON object has certain fields that are subarrays which represent relationships in the database.
Step 2: Send the JSON object via POST to Laravel for processing, here it gets tricky:
I can change the JSON object into an array first
$array = (array) $JSONobject;
Now I need to update, I would expect this to work:
Product::update($JSONobject->id,$array);
But because the array has subarrays, the update SQL that is executed cannot find the sub-array column in the table, it should instead look for the associated table. Can this be done? Or do I have to call the other models as well?
Thanks in advance!
This is something that Eloquent does not handle for you. The array that you supply to the update() method should contain columns only for, in your case, the Product model. You might try something like this to update relations. This is all off the top of my head and is by no means tested. Take it with a grain of salt.
$update = (array) $JSONobject;
$relations = [];
foreach ($update as $column => $value)
{
// If the value is an array then this is actually a relation. Add it to the
// relations array and remove it from the update array.
if (is_array($value))
{
$relations[$column] = $value;
unset($update[$column]);
}
}
// Get the product from the database so we can then update it and update any of the
// the products relations.
$product = Product::find($update['id']);
$product->update($update);
foreach ($relations as $relation => $update)
{
$product->{$relation}()->update($update);
}
The above code assumes that the key for your nested relation arrays is the name of the relation (method name used in your model). You could probably wrap this up in a method on your Product model. Then just call something like Product::updateRecursively($JSONobject); I'm terrible with names but you get the idea.
This probably won't work with more complex relations either. You'd have to take it a few steps further for things like many to many (or probably even one to many).
The following code will return an array of PHP Activerecord Objects:
$book = Book::find('all');
Assuming the program is aware of the order of books I can continue and update the attributes of the books and save them to the database as follows:
$book[0]->title = 'my first book';
$book[0]->author = 'Danny DeVito';
$book[4]->title = 'Nice Title';
in order to save the above I would have to invoke the ->save() method on each object
$book[0]->save();
$book[4]->save();
Is there a better way to do this? built-in PHP ActiveRecord function
that saves all members of a given array of objects, or based on an
association?
Assuming the original title of $book[4] above was already 'Nice
Title', would the ->save() method consider $book[4]changed and
continue with the database save?
Try using update all insted
$update = array();
$update['title'] = 'my first book';
$update['author'] = 'Danny DeVito' ;
$book[0]->update_all(array('set' =>$update));
$book[4]->update_all(array('set' =>array("title"=>"Nice Title"));
I think this should be cleaner
After much research I decided to post my conclusions/answers:
There is no such ActiveRecord library function that can update an
array of objects with unique values.
Assuming Activerecord would shoot one update request it would look like this:
UPDATE books
SET title = CASE id
WHEN 0 THEN 'my first book'
WHEN 4 THEN 'Nice Title'
END,
author = CASE id
WHEN 0 THEN 'Danny DeVito'
END
WHERE id IN (0,4)
The same question as "how would I update multiple rows with different values at once". This would go against the design of an Activerecord model, as an Object represents a row, and maps rows across tables. An obvious limitation for having such an easy model to work with.
Any assignment to an Object's attributes triggers a 'dirty' flag on
that attribute, and any subsequent call to update/save that
object will trigger a query even if the assigned attribute value is
the same as the database/model's previous value. Invoking the
save() method when no assignments were made does not trigger this
query.
I have table user which have fields username,password, and type. The type can be any or combination of these employee,vendor and client i.e a user can be vendor or client both or some another combination. For type field I have used the multiple checkbox, see the code below. This is the views/users/add.ctp file
Form->create('User');?>
Form->input('username');
echo $this->Form->input('password');
echo $this->Form->input('type', array('type' => 'select', 'multiple' => 'checkbox','options' => array(
'client' => 'Client',
'vendor' => 'Vendor',
'employee' => 'Employee'
)
));
?>
Form->end(__('Submit', true));?>
This is the code I have used in the model file. A callback method beforeSave
app/models/user.php
function beforeSave() {
if(!empty($this->data['User']['type'])) {
$this->data['User']['type'] = join(',', $this->data['User']['type']);
}
return true;
}
This code saves the multiple values as comma separated value in db.
The main problem comes when Im editing a user. If a user has selected multiple types during user creation I can't find the checkbox checked for that user types.
you should never be saving serialized data, json or csv in a field. This makes your life real hard later on down the line.
While habtm is one way to do things, if your binary maths is reasonable you might want to checkout bitmasks for this. here is a great post http://mark-story.com/posts/view/using-bitmasks-to-indicate-status
basics would be
1 = employee
2 = vendor
4 = client
// 8 = next_type
then, if the user was type employee & vendor the type would be 3 (1 + 2) and if it was a vendor & client the type would be 6 (2 + 4)
as you can see there is no way to mix it up, and bitwise works pretty good in mysql aswell so finds are pretty easy. See the post for much more detailed information
You should have a table types and a join table users_types.
What you're looking at is a HABTM relationship, so you should handle it like one.
In the joining UsersType model you should add a custom validation rule that checks if the current combination of types is allowed.
If you want to modify data after it's been found in the database, you can use the afterFind() callback in your model.
So in your case, put something like this is your user model:
function afterFind($results) {
$results['User']['type'] = explode(',', $results['User']['type']);
return $results;
}
There's more info on afterFind in the CakePHP manual.
That being said, it might be worth considering another approach, like a HABTM relationship as deceze first suggested above.