(link to previous question just in case: Struggling with one-to-many relation in an admin form)
I have this many-to-many relation in my Symfony-1.3 / Propel-1.4 project between User and Partner. When the User is being saved, if it has certain boolean flag being true, I want to clear all the links to the partners. Here is what I do at the moment and it doesn't work:
// inside the User model class
public function save(PropelPDO $con = null) {
if ($this->getIsBlaBla()) {
$this->setStringProperty(NULL);
$this->clearUserPartners();
}
parent::save($con);
}
Setting the string property to NULL works; looking at the DB clearly shows it. Thing is however, the USER_PARTNER table still holds the relations between the users and the partners. So I figured I have to clear the links one by one, like this:
foreach($this->getUserPartners() as $user_partner) {
$user_partner->delete();
//UserPartnerPeer::doDelete($user_partner); // tried that too
}
Both don't do the trick.
As I mentioned in my previous question, I am just monkey-learning Symfony via trial and error, so I evidently miss something very obvious. Please point me in the right direction!
EDIT: Here is how I made it work:
Moved the code to the Form class, like so:
public function doSave(PropelPDO $con = null) {
parent::doSave($con);
if ($this->getObject()->getIsSiteOwner()) {
$this->getObject()->setType(NULL);
$this->getObject()->save();
foreach($this->getObject()->getUserPartners() as $user_partner) {
$user_partner->delete();
}
}
return $this->getObject();
}
public function updateObject($values = null) {
$obj = parent::updateObject($values);
if ($obj->getIsSiteOwner()) {
$obj->clearUserPartners();
}
return $this->object;
}
What this does is:
When the boolean flag `is_site_owner` is up, it clear the `type` field and **saves** the object (ashamed I have not figured that out for so long).
Removes all existing UserPartner many-to-many link objects.
Clears newly associated (via the DoubleList) UserPartner relations.
Which is what I need. Thanks to all who participated.
Okey so now you have a many-to-many relation where in database terms is implemented into three tables (User , Parter and UserPartner). Same thing happens on Symfony and Propel, so you need to do something like this on the doSave method that should declare in UserForm:
public function doSave($con = null)
{
parent::doSave($con); //First all that's good and nice from propel
if ($this->getValue('please_errase_my_partners_field'))
{
foreach($this->getObject()->getUserPartners() as $user_partner_relation)
{
$user_partner_relation->delete();
}
}
return $this->getObject();
}
Check the method name "getUserPartners" that should be declared on the BaseUser.class.php (lib/model/om/BaseUser.class.php)
If you are learning Symfony, I suggest you use Doctrine instead of Propel because, I think Doctrine is simplier and more "beautiful" than Propel.
For your problem, I think you are on the good way. If I were you, I will keep my function save() I will write an other function in my model User
public function clearUserPartners(){
// You have to convert this query to Propel query (I'm sorry, but I don't know the right syntax)
"DELETE FROM `USER_PARTNER` WHERE user_id = '$this->id'"
}
With this function, you don't must use a PHP foreach.
But I don't understand what is the attribute StringProperty...
UserPartnerQuery::create()->filterByUser( $userObject )->delete();
or
UserPartnerQuery::create()->filterByUser( $partnerObject )->delete();
Had the same problem. This is a working solution.
The thing is that your second solution, ie. looping over the related objects and calling delete() on them should work. It's the documented way of doing things (see : http://www.symfony-project.org/book/1_0/08-Inside-the-Model-Layer#chapter_08_sub_saving_and_deleting_data).
But instead of bombing the DB with delete queries, you could just as well delete them in one go, by adding a method to your Peer class that performs the deletion using a simple DB query.
Related
Just wondering if it is possible that some kind of findOrNew for relationships exist in Eloquent (in case if relationship do not exist attach new model instance)?
What that mean:
Lets say that we have devices and specifications tables. Device belongs to specification. Specification_id is an FK (Know that is not best approach, but I have something like this left by previous programmer). Under id 11 we have device that do not have specification but we have to display that for user anyway.
$device = Device::find(11);
echo $device->specification->cpu;
In this case it will throw an error because specification will be null - it do not exist for device with id 11.
Know that I could check first if it exist but there a a lot of similar lines and app is pretty big. I need to move it from Kohana to Laravel. It works in Kohana because empty object is loaded then and 2nd line just return null. For Laravel I can just check if relationship exist and load new model then but I am curios if maybe there is any other and better way?
I would go for creating extra method in Device model this way:
public function getSpecification()
{
if ($device->specification) {
return $device->specification;
}
return Specification::find(20); // some default specification
// or
// return new Specification(['cpu' => 'Not provided']);
}
And now you could use it this way:
$device = Device::find(11);
$device->getSpecification()->cpu;
Of course it depends how would you need to use it. If you have many properties, you should run this method just once for object to not run multiple queries and in case you would use it for big collections you should also rethink improvements to lower database queries.
This doesn't quite create the related object as you requested, but for the purposes of outputting the data or replicating Kohana's null output in the absence of a related model, I tend to use the data_get() or object_get() helpers for this purpose.
$device = Device::find(11);
echo object_get($device->specification, 'cpu');
// You could probably do this too (untested)
echo object_get($device, 'specification.cpu');
Having had a bit of a look, you can override the getRelationshipFromMethod() method in Illuminate\Database\Eloquent\Model
protected function getRelationshipFromMethod($method)
{
// Different relationships return different types of data so
// tweak this as necessary. In theory you only care if the relationship
// type is a single entity rather than a collection.
$results = parent::getRelationshipFromMethod($method);
if ($results instanceOf Illuminate\Database\Eloquent\Collection) {
return $results;
}
// Generate a null value for any missing attributes
// PHP7 anonymous class. Return a real class < 7.0
return $this->relations[$method] = new class {
public function __get($attribute) {
return null;
}
};
// Or perhaps actually create a relationship with a specification
$this->relations[$method] = Specification::where('default', true)->first();
$this->specification()->associate($this->relations[$method]);
return $this->relations[$method];
}
We are trying to detect the changes in Laravel related models at attribute level, as we have to keep audit trail of all the changes which are made via the application.
We can track the changes via isDirty method on the Eloquent model for single model that is not related to any other model, but there is no way that we can track the changes on the related eloquent models. isDirty doesn't work on related models attributes. Can some one please help us on this?
Update to original question:
Actually we are trying to track changes on the pivot table that has extra attributes as well defined on it. IsDirty method doesn't work on those extra attributes which are defined in the pivot table.
Thanks
As much I understand your question, It's can achieve through Model Event and some sort of extra code with current and relation model.
Laravel Model Events
If you dont want to use any additional stuff, you can just use the Laravel Model Events (that in fact Ardent is wrapping in the hooks). Look into the docs http://laravel.com/docs/5.1/eloquent#events
Eloquent models fire several events, allowing you to hook into various
points in the model's lifecycle using the following methods: creating,
created, updating, updated, saving, saved, deleting, deleted,
restoring, restored.
Whenever a new item is saved for the first time, the creating and
created events will fire. If an item is not new and the save method is
called, the updating / updated events will fire. In both cases, the
saving / saved events will fire.
If false is returned from the creating, updating, saving, or deleting
events, the action will be cancelled:
Finally, reffering to your question you can utilize the above approaches in numerous ways but most obviously you can combine it (or not) with the Eloquent Models' getDirty() api docs here method and getRelation() api docs here method
It will work for example with the saving event.
Model::saving(function($model){
foreach($model->getDirty() as $attribute => $value){
$original= $model->getOriginal($attribute);
echo "Changed";
}
$relations = $model->getRelations();
foreach($relations as $relation){
$relation_model = getRelation($relation);
foreach($relation_model->getDirty() as $attribute => $value){
$original= $relation_model->getOriginal($attribute);
echo "Relation Changed";
}
}
return true; //if false the model wont save!
});
Another Thought might help you. when you saving
save() will check if something in the model has changed. If it hasn't it won't run a db query.
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
The getDirty() method simply compares the current attributes with a copy saved in original when the model is created. This is done in the syncOriginal() method:
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
check model is dirty isDirty():
if($user->isDirty()){
// changes have been made
}
Or check certain attribute:
if($user->isDirty('price')){
// price has changed
}
I did not check this code but hopeful to use as your answer by thoughts, if you have any confusion to deal such requirement or something need to optimize or change please let me know.
Two of my tables (clients and products) have a ManyToMany relation using Laravel's blongToMany and a pivot table.
Now I want to check if a certain client has a certain product.
I could create a model to check in the pivot table but since Laravel does not require this model for the belongsToMany method I was wondering if there is another way to check if a certain relationship exists without having a model for the pivot table.
I think the official way to do this is to do:
$client = Client::find(1);
$exists = $client->products->contains($product_id);
It's somewhat wasteful in that it'll do the SELECT query, get all results into a Collection and then finally do a foreach over the Collection to find a model with the ID you pass in. However, it doesn't require modelling the pivot table.
If you don't like the wastefulness of that, you could do it yourself in SQL/Query Builder, which also wouldn't require modelling the table (nor would it require getting the Client model if you don't already have it for other purposes:
$exists = DB::table('client_product')
->whereClientId($client_id)
->whereProductId($product_id)
->count() > 0;
The question is quite old but this may help others looking for a solution:
$client = Client::find(1);
$exists = $client->products()->where('products.id', $productId)->exists();
No "wastefulness" as in #alexrussell's solution and the query is more efficient, too.
Alex's solution is working one, but it will load a Client model and all related Product models from DB into memory and only after that, it will check if the relationship exists.
A better Eloquent way to do that is to use whereHas() method.
1. You don't need to load client model, you can just use his ID.
2. You also don't need to load all products related to that client into memory, like Alex does.
3. One SQL query to DB.
$doesClientHaveProduct = Product::where('id', $productId)
->whereHas('clients', function($q) use($clientId) {
$q->where('id', $clientId);
})
->count();
Update: I did not take into account the usefulness of checking multiple relations, if that is the case then #deczo has a way better answer to this question. Running only one query to check for all relations is the desired solution.
/**
* Determine if a Client has a specific Product
* #param $clientId
* #param $productId
* #return bool
*/
public function clientHasProduct($clientId, $productId)
{
return ! is_null(
DB::table('client_product')
->where('client_id', $clientId)
->where('product_id', $productId)
->first()
);
}
You could put this in you User/Client model or you could have it in a ClientRepository and use that wherever you need it.
if ($this->clientRepository->clientHasProduct($clientId, $productId)
{
return 'Awesome';
}
But if you already have defined the belongsToMany relationship on a Client Eloquent model, you could do this, inside your Client model, instead:
return ! is_null(
$this->products()
->where('product_id', $productId)
->first()
);
#nielsiano's methods will work, but they will query DB for every user/product pair, which is a waste in my opinion.
If you don't want to load all the related models' data, then this is what I would do for a single user:
// User model
protected $productIds = null;
public function getProductsIdsAttribute()
{
if (is_null($this->productsIds) $this->loadProductsIds();
return $this->productsIds;
}
public function loadProductsIds()
{
$this->productsIds = DB::table($this->products()->getTable())
->where($this->products()->getForeignKey(), $this->getKey())
->lists($this->products()->getOtherKey());
return $this;
}
public function hasProduct($id)
{
return in_array($id, $this->productsIds);
}
Then you can simply do this:
$user = User::first();
$user->hasProduct($someId); // true / false
// or
Auth::user()->hasProduct($someId);
Only 1 query is executed, then you work with the array.
The easiest way would be using contains like #alexrussell suggested.
I think this is a matter of preference, so unless your app is quite big and requires a lot of optimization, you can choose what you find easier to work with.
Hello all) My solution for this problem: i created a own class, extended from Eloquent, and extend all my models from it. In this class i written this simple function:
function have($relation_name, $id) {
return (bool) $this->$relation_name()->where('id','=',$id)->count();
}
For make a check existing relation you must write something like:
if ($user->have('subscribes', 15)) {
// do some things
}
This way generates only a SELECT count(...) query without receiving real data from tables.
To check the existence of a relationship between 2 models, all we need is a single query against the pivot table without any joins.
You can achieve it using the built-in newPivotStatementForId method:
$exists = $client->products()->newPivotStatementForId($product->id)->exists();
use trait:
trait hasPivotTrait
{
public function hasPivot($relation, $model)
{
return (bool) $this->{$relation}()->wherePivot($model->getForeignKey(), $model->{$model->getKeyName()})->count();
}
}
.
if ($user->hasPivot('tags', $tag)){
// do some things...
}
This has time but maybe I can help someone
if($client->products()->find($product->id)){
exists!!
}
It should be noted that you must have the product and customer model, I hope it helps,
I am using Laravel 4 and got stuck with a problem.
I have 3 models: User, Project, Task.
Relationships:
User belongsToMany('Project')
Project belongsToMany('User')
Project belongsToMany('Task')
Task belongsTo('Project')
I want to store a task but with no luck. The following code maybe tells in more detail what I want to accomplish:
Auth::user()->projects($projectId)->tasks()->save($task);
With that code I get:
Call to undefined method Illuminate\Database\Query\Builder::tasks()
Save your task first.
Then you can try...
Auth::user()->projects()->find($projectID)->tasks()->associate($task);
You may also need to modify your Project/Task relationship. The inverse of belongsToMany is always belongsToMany. I am thinking you will probably need hasMany and belongsTo instead.
Edit:
Sorry I think I lead you in the wrong direction, I finally got this working.
$task = new Task;
$task->name = 'Some super ultra task';
Auth::user()->projects()->where('projects.id', $projectID)->first()->tasks()->save($task);
Additionally, I think I made it harder than it needed to be.
$project = Project::find($projectID);
$project->tasks()->save($task);
That should work exactly the same. Since we know the ID of the project we are looking for, we shouldn't really need to go through the user model first.
To check that a user owns a project before saving, you can add this function to your Project model.
public function ownedByUser($user_id)
{
if(User::find($user_id)->projects()->count()) {
return true;
} else {
return false;
}
}
Then to you use it, here is what I did...
$project = Project::find($projectID);
if($project->ownedByUser(Auth::user()->id)) {
$project->tasks()->save($task);
} else {
echo 'This isn\'t your projec to edit, fool!';
}
Here is situation.... ...
I have a DBManager, which is implement a DBInterface, in the DBInterface, I got 4 method:
-create(DBCmd);
-read(DBCmd);
-update(DBCmd);
-delete(DBCmd);
The DBCmd object is responsible for generate the SQL statement, and the DBCmd requires an object in sql statement:
class DBCmd{
public _constructor($aObj){
}
public executeCreate(){
}
public executeRead(){
}
public executeUpdate(){
}
public executeDelete(){
}
}
The flow will be like this:
aObject ---> put it into DBCmd ----> put the DBCmd in DBManager ---> execute
But the problems happen when I get some objects related to other tables, for example...a customer have a purchase record, and which purchase record have many items....
So, what do I do in my read method? should I read all the records related to the customer?? Do I need to loop all the items inside the purchase record too?
If yes, when I doing read customer, I need to query 3 tables, but some that may not need to see.....it waste the resource...
And I come up with another solution, I make a new set of DBCmd, that allow me to get the related DB items, for example:
class getReleatedPurchaseRecordDBCmd{
public _constructor($aCustomerObject){
}
//.... ....
}
But in this "solution", I got some problems, is I loss the relationship in the object customer...yes, I can read back all the records, get the customer object basically don't know any things about the purchase record....
Some may ask me to do something like this:
class customer{
//skip other methods...
public getPurchaseRecords(){
//query the db
}
}
It works, but I don't want the object structure have some strong relationship between the db....That's why I come up with the DBCmd stuff...
So, everything seems to be very coupling, how can solve it? Thank you.
for stuff like this i tend to get the count of sub objects with the initial query usually involving sql COUNT and JOIN, then have a seperate getSubObjects command that can be called if needed later. So for example:
$datamodel->getCustomer($id);//or some such method
returns
class Customer{
$id = 4;
$recordCount = 5;
$records = null;
}
I can then use the count for any display stuff as needed, and if i need the records populated call:
$customer->records = $datamodel->getCustomerRecords($customer->id);