Yii2 REST ActiveRecord deep relation not working - php

We've been coding an API service with Yii2 and created all model classes extending from ActiveRecord.
Next we started adding the simple relations. This is where things got strange.
We have 3 tables (limited to explain the problem) 'app_customers', 'lu_postcodes' and 'lu_countries'.
A customer has one postcode_id.
A postcode has one country_id.
In the customer model we would like to add relationships to get the postcode and country data when getting customer data.
Customer model:
namespace api\modules\v1\models;
use Yii;
use \api\common\components\BaseModel;
/**
*
*/
class Customer extends BaseModel {
public function extraFields() {
return ['postcode'];
}
public function getPostcode() {
return $this->hasOne(Postcode::className(), ['id' => 'postcode_id'])
->with(['country']);
}
....[more code]
Postcode model:
namespace api\modules\v1\models;
use Yii;
use \api\common\components\BaseModel;
/**
*
*/
class Postcode extends BaseModel {
public function extraFields() {
return ['country'];
}
public function getCountry() {
return $this->hasOne(Country::className(), ['id' => 'country_id']);
}
....[more code]
So when calling v1/customer?expand=postcode it returns all customer data and postcode is populated with a postcode object. But we can't get the country data to load together with the postcode data.
In an ideal situation we would like the index and view actions from customer to include both postcode and country data. (did not try yet .. one step at a time :) )
Trying to debug this issue We dumped the sql from Customer::getPostcode
with var_dump($q->createCommand()->sql); and got the output:
'SELECT * FROM `lu_postcodes` WHERE `id`=:qp0'
This might have something to do with missing tablenames?
In earlier attempt I managed to get the country data loaded, but it used the ID from customer which resulted in a wrong country obviously.
Any ideas? Thanks in advance!
I did some research and found https://github.com/yiisoft/yii2/issues/6844#issuecomment-131482508, which looked promising, but once implemented it still remained the same result.
----------------------- EDIT BELOW
I tried all options and in most cases I get a JSON parse error from Yii. In one case I get a result like
...
"display_name": "Rxxxxxxx xxxxxxx",
"postcode": {
"id": 361,
"country_id": 20,
"state_id": 2,
"zip": "3945",
"city": "Ham",
"alpha": "ham",
"longitude": "5.1730329000",
"latitude": "51.0966039000"
},
"country": null
...
Used option: Adding country in extraFields() in Customer.
public function extraFields() {
return ['postcode', 'country' => function($model) { return $model->postcode->country; }];
}

Yii2 itself does not directly support nested relations . It is not good idea to build such complex objects in a REST API using ActiveRecord classes. Remember you will likely have a collection API i.e. a group of customer , each customer will need multiple subqueries to satisfy the request.
There are multiple ways to address this.
Use separate controller to solve this /customer/<id>/country. This is good solution if country/ postcode is a hasMany relationship
Define country in the default fields() functions of postcode instead of extraFields(). This way when you pass expand?=postcode, both postcode and country will always show in the expanded output
Use ad-hoc expansion function in your extraFields() definition of Customer Class with something like this
.
public function extraFields()
{
return [
'postcode',
'country'=>function($model){
return $model->postcode->country;
}
];
}
Define a country function in your Customer class using Via Relation, this is more useful when you have hasMany relationships
.
public function getCountry()
{
return $this->hasOne(Country::className(), ['id' => 'country_id'])
->via('postCode');
}
Define a custom query with the exact parameters you desire and use a dataProvider as the response to your Index function something like this
..
public function actionIndex()
{
$query = (new \yii\db\Query())
// This can also be a ActiveQuery
// using Customer::find() with addSelects() etc
// ... additional query conditions
return new ActiveDataProvider(['query'=>$query]);
}
Similarly
public function actionView($id)
{
$query = (new \yii\db\Query()) // simlar to above
$query->andWhere(['customer.id'=>(int)$id]); // or similar condition
return $query->one();
}

I got it working.
It was an encoding issue...
'charset' => 'UTF-8', in the db config array did the trick.

Related

How to cast data from array in Laravel / Eloquent in model

Can someone please tell me how can I achieve something in a Laravel Model?
One of the columns contains json_encoded data:
["1", "7", "13", "18"]
I have a model for this specific table and when I'm trying to get the data from different tables based on those ids, it won't work as variable is always null:
use App\Models\VehiculeType;
...
class Freight extends Model
{
...
public function selected_vt() {
return VehiculeType::whereIn('id', json_decode($this->vehicules))
->get();
}
}
Then in the controller:
$freights = Freight::with('selected_vt')
->where('user_id', Auth::user()->id)
->get();
And I'm getting this error:
Argument 1 passed to Illuminate\Database\Query\Builder::cleanBindings() must be of the type array, null given
I am sure I'm making some fundamental error trying to do this in the completely wrong way.
Unfortunately you can't use with with collection, and selected_vt must return relation instance rather than a collection.
Try eloquent-json-relations package, it can helps you with your situation:
https://github.com/staudenmeir/eloquent-json-relations
Here is an example depending your situation
After installing package
in Freight Modle
class Freight extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
protected $casts = [
'vehicules' => 'json'
];
public function vehicules()
{
return $this->belongsToJson(VehiculeType::class, 'vehicules');
}
}
If you want the reversed relation
in VehiculeType Model
class VehiculeType extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
public function freights()
{
return $this->hasManyJson(Freight::class, 'vehicules');
}
}
Then in the controller:
$freights = Freight::with('vehicules')
->where('user_id', Auth::user()->id)
->get();

Modify Laravel model response to use different ID

I'm looking to sort of "intercept" and change a field in a model before it's send back to the client. I have an API with endpoints similar to the following:
Route::get('users/{user}', 'Api\UserController#user');
My application uses vinkla/laravel-hashids, so as far as the client application is concerned, the ID to query for should be something like K6LJwKJ1M8, and not 1. Currently I can query for a user providing the hash-id, but the response comes back with the numeric/auto-incrementing ID.
e.g. For a query such as /api/users/K6LJwKJ1M8 I receive the following response:
{
"id": 1,
"address": null,
"telephone": null,
"name": "TestNameHere",
"description": null,
...
}
Is there a nice way in Laravel that I could modify all user objects being returned in responses to use the vinkla/laravel-hashids ID, instead of the auto-incrementing ID?
Ideally, the above response would become:
{
"id": K6LJwKJ1M8,
"address": null,
"telephone": null,
"name": "TestNameHere",
"description": null,
...
}
I was thinking something like using getRouteKey would work, but it doesn't change the object that's sent out in the JSON response.
e.g.
public function getRouteKey() {
return Hashids::encode($this->attributes['id']);
}
It'd be nice if there was one place to change this since my application has around 40 different routes that I'd otherwise need to change "manually" (e.g. before sending the response do something like $user->id = Hashids::encode($id))
You can implement the CastsAttributes interface:
Classes that implement this interface must define a get and set method. The get method is responsible for transforming a raw value from the database into a cast value, while the set method should transform a cast value into a raw value that can be stored in the database.
Inside your app directory create a directory named Casts and create a class named UserCode:
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use ...\Hashids;
class UserCode implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return Hashids::encode($value);
}
public function set($model, $key, $value, $attributes)
{
return Hashids::decode($value);
}
}
Then in your user model add the UserCode cast to the id value:
use App\Casts\UserCode; // <--- make sure to import the class
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $casts = [
'id' => UserCode::class,
];
}
Then if you would do User::find(1)->id you will get the hashed value or visit /user/1 it will return the user with the hashed id. credit.
Note that you can't find the user by the hashed id unless you implemented something e.g. /user/hashed_id will give 404.
You can use API Resources
https://laravel.com/docs/7.x/eloquent-resources#introduction
API Resource acts as a transformation layer that sits between your
Eloquent models and the JSON responses that are actually returned to
your application's users.
You may create an API resource for the user and use it wherever you're returning the user in the response.
Api resources gives you a lot more control, you could manipulate whatever field you want, send some extra fields using the combination of a few fields, change the name of the fields that you want in your response (xyz => $this->name)
UserResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
//You can access model properties directly using $this
return [
'id' => Hashids::encode($this->id),
"address": $this->address,
"telephone": $this->telephone,
"name": $this->name,
"description": $this->description,
...
];
}
}
And then wherever you want to return a user instance.
Controller
// $user is a User Model Instance.
return new UserResource($user);
In case you have a collection
// $users is a collection of User Model instances.
return UserResource::collection($users);
UserResource will be applied for every model instance in your collection and then returned to you as your JSON response.
To achieve what you want, in your model, you would have to use set getIncrementing to false and ensure getKeyType is set to string.
class YourModel extends Model
{
public function getIncrementing()
{
return false;
}
public function getKeyType()
{
return 'string';
}
}
The above would work if you were using uuid and have the following in your migration file:
$table->uuid('id')->primary();
You would probably have to find a way to modify the package to use this approach.

OctoberCms record finder with dynamic scope

I'm using OctoberCMS based on Laravel and trying to get a list of products within a form via Record Finder.
The use-case is that the record-finder must show available products based on dynamic condition.
I tried to achieve this via "Scope" option of record finder for related form model but not finding a way to pass the dynamic value to the scope.
Sample Code --
class A extends Model
{
public $belongsTo = [
'product' => [
'Plugin\Models\B',
'key' => 'id',
'scope' => 'specificProduct'
],
];
}
class B extends Model
{
public function scopeSpecificProduct($query , $product_type)
{
return $query->where('product_type', $product_type);
}
}
Here $product_type is the dynamic value which I am trying to pass via record finder and get in scope.
Can anyone suggest that is this a correct way for such requirement or how should I achieve this ?
In your fields definition you have to use the scope attribute
fields:
products:
label: Products
type: recordfinder
scope: specificProduct
With this, the second param of your scope will be the A model that is creating or updating
class B extends Model
{
public function scopeSpecificProduct($query , $model)
{
return $query->where('product_type', $model->depend_attribute);
}
}

list more than one field in lists laravel method

I am using Zizaco/entrust laravel package and suppose I want to fetch users with teacher role.
I want return selected users and their name and family combination like this :
[
1 => 'ali nasiri',
2 => 'majid basirati'
]
For that, I wrote this code :
$teachers =
Role::where('name', 'teacher')->first()->users()->lists('name', 'users.user_id');
But that returns like this :
{
"1": "ali",
"2": "majid"
}
Means that only name field was returned because we can define one field in first Parameter of lists method.
How Can I do that and what is best solution ?
In your user model, add a custom attribute(accessor):
<?php
use Zizaco\Entrust\Traits\EntrustUserTrait;
class User extends Eloquent
{
use EntrustUserTrait; // add this trait to your user model
...
protected $appends = ['full_name'];
//Setter of full name attribute
public function getFullNameAttribute()
{
return $this->name.' '.$this->surname;
}
}
Then use it to access the full name:
$teachers = User::whereHas('roles',function($q){
return $q->where('name','teacher');
})
->get()
->lists('full_name');

There are <select> populated with (key->value), and field "key" in a model. How I can display it's "value" in a view, instead of "key"?

In model:
public function getOptionsGender()
{
array(0=>'Any', 1=>Male', 2=>'Female');
}
In view (edit):
echo $form->dropDownList($model, 'gender', $model->optionsGender);
but I have a CDetailView with "raw" attributes, and it displays numbers instead of genders.
$attributes = array(
...
'gender',
)
What is appropriate way to convert these numbers back to genders? Should I do it in a model, replacing fields such as $this->gender = getOptionsGender($this->gender)? Any github examples will be very appreciated.
I had to choose gender, age, city, country etc. in a few views that are not related to this one. Where should I place my getOptionsGender function definitions?
Thank for your help, the problem is solved.
In model:
public function getGenderOptions() { ... }
public function genderText($key)
{
$options = $this->getGenderOptions();
return $options[$key];
}
In view:
$attributes = array(
array (
'name'=>'gender',
'type'=>'raw',
'value'=>$model->genderText($model->gender), //or $this->genderText(...)
),
);
$this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>$attributes,
));
The working example can be found here:
https://github.com/cdcchen/e23passport/blob/c64f50f9395185001d8dd60285b0798098049720/protected/controllers/UserController.php
In Jeffery Winsett's book "Agile Web Application Development with Yii 1.1", he deals with the issue using class constants in the model you are using. In your case:
class Model extends CActiveRecord
{
const GENDER_ANY=0;
const GENDER_MALE=1;
const GENDER_FEMALE=2;
public function getGenderOptions(){
return array(
self::GENDER_ANY=>'Any',
self::GENDER_MALE=>'Male',
self::GENDER_FEMALE=>'Female',
);
}
public function getGenderText(){
$genderOptions=$this->genderOptions();
return isset($genderOptions[$this->gender]) ? $genderOptions[$this->gender] : "unkown gender({$this->gender})";
}
}
Then in your CDetailView you would have to alter it from gender to:
array(
'name'=>'gender',
'value'=>CHtml::encode($model->genderText()),
),
If several models have the same data, you may want to create a base model that extends CActiveRecord and then extend the new model instead of CActiveRecord. If this model is the only one with that data (ie User model only has gender), but other views use that model to display data, then I would leave it just in the single model class. Also keep in mind that if you place getGenderOptions in the extended class, and you extend ALL your models, they will all have that option available, but may not have the attributes needed and will throw an error if you aren't checking for it.
All this being said, I still think it is a matter or preference. You can handle it however you want, wherever you want. This is just one example from a book I have specifically on Yii.

Categories