Yii2 REST API update (put) when composite key - json

I am trying to update my model via PUT http request in Yii2 framework.
Everything works fine when I have single Primary Key in my model.
Problems are when I have composite primary key in table.
How to update?
I submit JSON:
{"date_execution":"2017-08-26","order_id":"59", "company_id":13,"your_price":100,"car_id":"8","note":"lorem ipsum"}
my composite primary key include:
- order_id
- company_id
I tried following requests:
PUT SERVER/offer/100 - where 100 is company_id
PUT SERVER/offer/2000 - where 2000 is order_id
those 2 requests are returning problem:
{"name":"Not Found","message":"Object not found: 13","code":0,"status":404,"type":"yii\\web\\NotFoundHttpException"}
I also tried
PUT SERVER/offer/2000/100 - where 2000 is order_id and 100 is company_id
PUT SERVER/offer/100/2000
those 2 return controller/action not found exception
Also I added order_id and company_id to JSON,
but nothing works.
Controller Class:
use yii\rest\ActiveController;
class OfferController extends ActiveController
{
// adjust the model class to match your model
public $modelClass = 'app\models\Offer';
public function behaviors(){
$behaviors = parent::behaviors();
// remove authentication filter
$auth = $behaviors['authenticator'];
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => CustomCors::className()
];
// re-add authentication filter
$behaviors['authenticator'] = [
'class' => CompositeAuth::className(),
'authMethods' => [
HttpBearerAuth::className(),
],
];
// avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
$behaviors['authenticator']['except'] = ['options'];
return $behaviors;
}
}

It should work if you use PUT SERVER/offer/2000,100
You can print primaryKey() of the model to know the order of the keys.
You can see it in the docs here
https://www.yiiframework.com/doc/api/2.0/yii-rest-action
If composite primary key, the key values will be separated by comma.

yii\rest\UpdateAction uses ActiveRecord::findModel() method to load data. There is an answer in those phpdoc:
If the model has a composite primary key, the ID must be a string of
the primary key values separated by commas
So the right resource is (considering the first key field in the table structure is company_id)
PUT SERVER/offer/100,2000

You firstly need to add primaryKey() in the model, to override the default primaryKey() of the ActiveRecord class. This function needs to return your composite primary key.
What you need to do the model thus would be
primaryKey()
{
return array('company_id', 'order_id');
}

Related

django admin site returns MultipleObjectsReturned exception with inspectdb imported legacy database and composite primary key

Using inspectdb, I have imported a legacy database, that contains entities with composite primary keys, in django . The database schema contains about 200 different entities and inspectdb is quite handy in that situation.
This is the schema in mysql:
CREATE TABLE `mymodel` (
`id` bigint(20) unsigned NOT NULL DEFAULT '0',
`siteid` bigint(20) unsigned NOT NULL DEFAULT '0',
...
PRIMARY KEY (`siteid`,`id`),
...
Following the autogenerated model in django (imported using python manager.py inspectdb)
class Mymodel(models.Model):
id = models.PositiveBigIntegerField()
siteid = models.PositiveBigIntegerField(primary_key=True)
...
class Meta:
managed = False
db_table = 'mymodel'
unique_together = (('siteid', 'id'),
I have registered all models in the admin site using the following approach:
from django.contrib import admin
from django.apps import apps
app = apps.get_app_config('appname')
for model_name, model in app.models.items():
admin.site.register(model)
After all the work is done, I navigate to the admin site and click on any object in the "mymodel" section and the following exception will be returned:
appname.models.Content.MultipleObjectsReturned: get() returned more than one Mymodel-- it returned more than 20!
Obviously, (this is what it seems to me at least) admin is using the siteid to get the object, tough it should use the unique_together from the Meta class.
Any suggestions how I can achieve to solve this with a general configuration and get the admin site module to query using the unique_together?
Yes you can solve this problem but you put a little more effort.
First you separate model-admin class for model Mymodel and customize model-admin class method:
Since django admin build change url in ChangeList class, So we can create a custom Changelist class like MymodelChangelist and pass id field value as a query params. We will use id field value to getting object.
Override get_object() method to use custom query for getting object from queryset
Override get_changelist() method of model-admin to set your custom Changelist class
Override save_model() method to save object explicitly.
admin.py
class MymodelChangelist(ChangeList):
# override changelist class
def url_for_result(self, result):
id = getattr(result, 'id')
pk = getattr(result, self.pk_attname)
url = reverse('admin:%s_%s_change' % (self.opts.app_label,
self.opts.model_name),
args=(quote(pk),),
current_app=self.model_admin.admin_site.name)
# Added `id` as query params to filter queryset to get unique object
url = url + "?id=" + str(id)
return url
#admin.register(Mymodel)
class MymodelAdmin(admin.ModelAdmin):
list_display = [
'id', 'siteid', 'other_model_fields'
]
def get_changelist(self, request, **kwargs):
"""
Return the ChangeList class for use on the changelist page.
"""
return MymodelChangelist
def get_object(self, request, object_id, from_field=None):
"""
Return an instance matching the field and value provided, the primary
key is used if no field is provided. Return ``None`` if no match is
found or the object_id fails validation.
"""
queryset = self.get_queryset(request)
model = queryset.model
field = model._meta.pk if from_field is None else model._meta.get_field(from_field)
try:
object_id = field.to_python(object_id)
# get id field value from query params
id = request.GET.get('id')
return queryset.get(**{'id': id, 'siteid': object_id})
except (model.DoesNotExist, ValidationError, ValueError):
return None
def save_model(self, request, obj, form, change):
cleaned_data = form.cleaned_data
if change:
id = cleaned_data.get('id')
siteid = cleaned_data.get('siteid')
other_fields = cleaned_data.get('other_fields')
self.model.objects.filter(id=id, siteid=siteid).update(other_fields=other_fields)
else:
obj.save()
Now you can update any objects and also add new object. But, On addition one case you can't add- siteid which is already added because of primary key validation

Laravel 5.8 many to many relation using custom column names for pivot table

I have two tables user_details and location as below:
user_details
user_id (INTEGER PRIMARY KEY)
......
location
id (INTEGER UNSIGNED PRIMARY KEY)
......
These two tables are related to one another by many to may relationship. Now I am using a pivot table location_user_details as shown below
location_user_details
id (INTEGER UNSIGNED PRIMARY KEY),
user_id (INTEGER FOREIGN KEY REFERENCES use_details.user_id)
location_id (INTEGER FOREIGN KEY REFERENCES location.id)
type (VARCHAR)
The relation code for model UserDetails is as below:
class UserDetails extends Model
{
protected $primaryKey = 'user_id';
........
public function location(){
return $this->belongsToMany(Location::class, 'location_user_details', 'user_id', 'id')->withPivot('type');
}
}
The relation code for model Location is as below:
class Location extends Model
{
......
public function userDetails(){
return $this->belongsToMany(UserDetails::class, 'location_user_details', 'id', 'user_id')->withPivot('type');
}
}
Now in my controller, I am creating the relation after saving both UserDetails and Location object by the following code:
$userDetails->location()->attach($location->id, ['type'=>'HEADQUARTER']);
When the code reaches this line of execution, I get the following exception:
SQLSTATE[HY000]: General error: 1364 Field 'location_id' doesn't have a default value (SQL: insert into location_user_details (id, type, user_id) values (1, HEADQUARTER, 0))
I want to point out to two things that are happening.
The location field is not getting into the query to insert data.
The user_id value is 0 which should be greater than 0 as the data is getting inserted into the user_details table.
Now let me mention here that when I go to the database to look up for data in location and user_details table, I find all the data has been inserted.
Now my question is what has possible gone wrong with this code?
Well at last I have found what went wrong. There is not much documentation on this topic. Actually it lies with the use of the function belongsToMany. The belongsToManyfunction takes up-to 7 parameters. Till now I could decode the first 4 parameters.
The first parameter takes the class of the related model.
The second parameter takes the name of the pivot table in the database.
The third parameter takes the name of the column in the pivot table that references the model on which belongsToMany is called.
The fourth parameter takes the name of the column in the pivot table that references the related model.
So as a result the code for function location in the model UserDetails would look like :
public function location(){
return $this->belongsToMany(Location::class, 'location_user_details', 'user_id', 'location_id')->withPivot('type');
}
And the code for function userDetails in the model Location would look like :
public function userDetails(){
return $this->belongsToMany(UserDetails::class, 'location_user_details', 'location_id', 'user_id')->withPivot('type');
}
I hope this would be helpful to others.
The problem may be in the variable $location, but it is not shown as you create it. Make sure it is a Location Object

I want to get Id of primary table but get null and Trying to get property of non-object

Here is my controller code. Can someone help me? how can i get primary table id. I have primary table with one id and secondary table called articles in which all fields are shown here in controller code how can i get id of primary table(the relationship is primary and foreign key relationship)
public function store(Request $request)
{
$article = new Article;
$article->user_id = Auth::user()->id;
$article->content = $request->content;
$article->live = (boolean)$request->live;
$article->post_on = $request->post_on;
dd($article->user_id);
$article->save();
// Article::create($request->all());
}
You need to create an inverse relationship e.g. belongsTo in your Article model which attributes that article to your User model.
https://laravel.com/docs/5.4/eloquent-relationships#one-to-one
See the Defining The Inverse Of The Relationship section.
Then you can do something like $article->user->id.
If your relationships are already defined, use $article->fresh('user') after the save() method.

Multiple Foreign Key to same table Gas Orm

Since this mornong i am facing a very big problem. I am using CodeIgniter to develop a website, and GAS ORM for the database.
I have basically two tables. One named "pool", and one named "partners". I am having two associations between these two tables, so I have two foreign keys in my table Partners referencing the table pool.
Pool(#id:integer, name:varchar)
Partners(#id:integer, associated_pool_id=>Pool, futur_associated_pool_id=>Pool).
As I have two references to the same table, I can't name the foreign keys "pool_id". So in my relationships with Gas ORM, I have to specify the names of the columns. I do it, but it doesn't work...
Here is what I do:
class Partner extends ORM {
public $primary_key = 'id';
public $foreign_key = array('\\Model\\Pool' => 'associated_pool_id', '\\Model\\Pool' => 'future_associated_pool_id');
function _init()
{
// Relationship definition
self::$relationships = array(
'associated_pool' => ORM::belongs_to('\\Model\\Pool'),
'future_association_pool' => ORM::belongs_to('\\Model\\Pool'),
);
self::$fields = array(
'id' => ORM::field('auto[11]'),
'name' => ORM::field('char[255]'),
'associated_pool_id' => ORM::field('int[11]'),
'future_associated_pool_id' => ORM::field('int[11]')
);
}
and in my Pool class :
class Pool extends ORM {
public $primary_key = 'id';
function _init()
{
// Relationship definition
self::$relationships = array(
'associated_partner' => ORM::has_many('\\Model\\Partner'),
'future_associated_partner' => ORM::has_many('\\Model\\Partner'),
);
self::$fields = array(
'id' => ORM::field('auto[11]'),
'name' => ORM::field('char[50]'),
);
}
I have a test controller testing if everything is okay:
class Welcome extends CI_Controller {
public function index()
{
$pool = \Model\Pool::find(1);
echo $pool->name;
$partners = $pool->associated_partner();
var_dump($partners);
}
But I have an error saying:
Error Number: 1054
Champ 'partner.pool_id' inconnu dans where clause
SELECT * FROM partner WHERE partner.pool_id IN (1)
I don't know how to specify to Gas ORM that it shouldn't take "pool_id" but "associated_pool_id"....
Thank you for your help!!!!!!!!!!!!
I don't know, if this topic is still up to date and interesting to some of you, but in general, I had the exact same problem.
I decided Gas ORM to be my mapper in combination with CodeIgniter. As my database structure was given and it was not following the table_pk convention of Gas, I had to define a foreign key by myself which shall refer to my custom database foreign key. However, the definition of it had no impact on anything. Like your error above, the mapper was not able to build the right SQL-statement. The statement looked similar to yours:
SELECT * FROM partner WHERE partner.pool_id IN (1)
Well, it seems like Gas ignores the self-defined foreign keys and tries to use the default table_pk convention. This means, it takes the table (in your case: pool) and the primary key (id) by merging it with a underscore character.
I figured out, that the constructor of orm.php handles every primary and foreign key defined within the entities. In line 191, the code calls an if clause combined with the empty function of php. As the primary key is defined always and there is no negation in the statement, it skips the inner part of the clause every time. However, the inner part takes care of the self-defined foreign keys.
Long story short, I added a negation (!) in line 191 of orm.php which leads me to the following code:
if ( ! empty($this->primary_key))
{
if ( ! empty($this->foreign_key))
{
// Validate foreign keys for consistency naming convention recognizer
$foreign_key = array();
foreach($this->foreign_key as $namespace => $fk)
{
$foreign_key[strtolower($namespace)] = $fk;
}
$this->foreign_key = $foreign_key;
}
else
{
// If so far we didnt have any keys yet,
// then hopefully someone is really follow Gas convention
// while he define his entity relationship (yes, YOU!)
foreach ($this->meta->get('entities') as $name => $entity)
{
if ($entity['type'] == 'belongs_to')
{
$child_name = $entity['child'];
$child_instance = new $child_name;
$child_table = $child_instance->table;
$child_key = $child_instance->primary_key;
$this->foreign_key[strtolower($child_name)] = $child_table.'_'.$child_key;
}
}
}
}
Well, this little fix helped me out a lot and I hope some of you can take advantage of this hint as well.

Preventing malicious users update data at add action

Here is a basic add action:
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->data);
if ($this->Articles->save($article)) {
$this->Flash->success('Success.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('Fail.');
}
}
$this->set(compact('article'));
}
If a malicious user injects at form a field with name id and set the value of this field to 2. Since the user do that the id value will be in $this->request->data so at $this->Articles->patchEntity($article, $this->request->data) this id will be patched and at $this->Articles->save($article) the record 2 will be updated instead of create a new record??
Depends.
Entity::$_accessible
If you baked your models, then this shouldn't happen, as the primary key field will not be included in the entities _accessible property, which defines the fields that can be mass assigned when creating/patching entities. (this behavior changed lately)
If you baked your models, then this shouldn't happen, as the primary key field(s) will be set to be non-assignable in the entities _accessible property, which means that these the fields cannot be set via mass assignment when creating/patching entities.
If you didn't baked your models and haven't defined the _accessible property, or added the primary key field to it, then yes, in case the posted data makes it to the patching mechanism, then that is what will happen, you'll be left with an UPDATE instead of an INSERT.
The Security component
The Security component will prevent form tampering, and reject requests with modified forms. If you'd use it, then the form data wouldn't make it to the add() method in the first place.
There's also the fieldList option
The fieldList option can be used when creating/patching entities in order to specifiy the fields that are allowed to be set on the entity. Sparse out the id field, and it cannot be injected anymore.
$article = $this->Articles->patchEntity($article, $this->request->data, [
'fieldList' => [
'title',
'body',
//...
]
]);
And finally, validation
Validation can prevent injections too, however that might be considered a little wonky. A custom rule that simply returns false would for example do it, you could create an additional validator, something like
public function validationAdd(Validator $validator) {
return
$this->validationDefault($validator)
->add('id', 'mustNotBePresent', ['rule' => function() {
return false;
}]);
}
which could then be used when patching the entity like
$article = $this->Articles->patchEntity($article, $this->request->data, [
'validate' => 'add'
]);